Refactor World-Management and Region/Chunk-Loading (#496)

* Implement PackedIntArrayAccess

* First working render with BlueNBT

* Progress converting chunkloaders

* Core rewrite done

* WIP - Restructuring configs and world-map mapping

* WIP - Compiling and starting without exceptions :)

* Fix cave detection

* Ensure configuration backwards compatibility (resolve dimension from configured world if missing)

* Implement support for 1.16+ chunks

* Implement support for 1.15+ chunks

* Implement support for 1.13+ chunks and some fixes

* Also find worlds based on their id again in BlueMapAPI

* Improve autogenerated config names

* Implement equals for all ServerWorld implementations

* Get rid of var usage
This commit is contained in:
Lukas Rieger 2024-02-07 20:43:37 +01:00 committed by GitHub
parent efd45658d5
commit 16981f2797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
177 changed files with 3665 additions and 4022 deletions

View File

@ -33,6 +33,9 @@ dependencies {
api ("de.bluecolored.bluemap.core:BlueMapCore")
compileOnly ("org.jetbrains:annotations:16.0.2")
compileOnly ("org.projectlombok:lombok:1.18.28")
annotationProcessor ("org.projectlombok:lombok:1.18.28")
testImplementation ("org.junit.jupiter:junit-jupiter:5.8.2")
testRuntimeOnly ("org.junit.jupiter:junit-jupiter-engine:5.8.2")

View File

@ -26,10 +26,16 @@
import de.bluecolored.bluemap.common.config.*;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.core.MinecraftVersion;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Map;
public interface BlueMapConfigProvider {
public interface BlueMapConfiguration {
MinecraftVersion getMinecraftVersion();
CoreConfig getCoreConfig();
WebappConfig getWebappConfig();
@ -42,4 +48,8 @@ public interface BlueMapConfigProvider {
Map<String, StorageConfig> getStorageConfigs();
@Nullable Path getResourcePacksFolder();
@Nullable Path getModsFolder();
}

View File

@ -36,17 +36,17 @@
import de.bluecolored.bluemap.common.config.MapConfig;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.debug.StateDumper;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.resources.datapack.DataPack;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurateException;
@ -60,7 +60,6 @@
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
@ -72,85 +71,32 @@
@DebugDump
public class BlueMapService implements Closeable {
private final ServerInterface serverInterface;
private final BlueMapConfigProvider configs;
private final Map<Path, String> worldIds;
private final Map<String, Storage> storages;
private volatile WebFilesManager webFilesManager;
private Map<String, World> worlds;
private Map<String, BmMap> maps;
private final BlueMapConfiguration config;
private final WebFilesManager webFilesManager;
private ResourcePack resourcePack;
private final Map<String, World> worlds;
private final Map<String, BmMap> maps;
private final Map<String, Storage> storages;
public BlueMapService(ServerInterface serverInterface, BlueMapConfigProvider configProvider, @Nullable ResourcePack preloadedResourcePack) {
this(serverInterface, configProvider);
if (preloadedResourcePack != null)
public BlueMapService(BlueMapConfiguration configuration, @Nullable ResourcePack preloadedResourcePack) {
this(configuration);
this.resourcePack = preloadedResourcePack;
}
public BlueMapService(ServerInterface serverInterface, BlueMapConfigProvider configProvider) {
this.serverInterface = serverInterface;
this.configs = configProvider;
public BlueMapService(BlueMapConfiguration configuration) {
this.config = configuration;
this.webFilesManager = new WebFilesManager(config.getWebappConfig().getWebroot());
this.worldIds = new ConcurrentHashMap<>();
this.storages = new HashMap<>();
this.worlds = new ConcurrentHashMap<>();
this.maps = new ConcurrentHashMap<>();
this.storages = new ConcurrentHashMap<>();
StateDumper.global().register(this);
}
public String getWorldId(Path worldFolder) throws IOException {
// fast-path
String id = worldIds.get(worldFolder);
if (id != null) return id;
// second try with normalized absolute path
worldFolder = worldFolder.toAbsolutePath().normalize();
id = worldIds.get(worldFolder);
if (id != null) return id;
// secure (slower) query with real path
worldFolder = worldFolder.toRealPath();
id = worldIds.get(worldFolder);
if (id != null) return id;
synchronized (worldIds) {
// check again if another thread has already added the world
id = worldIds.get(worldFolder);
if (id != null) return id;
Logger.global.logDebug("Loading world id for '" + worldFolder + "'...");
// now we can be sure it wasn't loaded yet .. load
Path idFile = worldFolder.resolve("bluemap.id");
if (!Files.exists(idFile)) {
id = this.serverInterface.getWorld(worldFolder)
.flatMap(ServerWorld::getId)
.orElse(UUID.randomUUID().toString());
Files.writeString(idFile, id, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
worldIds.put(worldFolder, id);
return id;
}
id = Files.readString(idFile);
worldIds.put(worldFolder, id);
return id;
}
}
public WebFilesManager getWebFilesManager() {
if (webFilesManager == null) {
synchronized (this) {
if (webFilesManager == null)
webFilesManager = new WebFilesManager(configs.getWebappConfig().getWebroot());
}
}
return webFilesManager;
}
@ -164,13 +110,13 @@ public synchronized void createOrUpdateWebApp(boolean force) throws Configuratio
}
// update settings.json
if (!configs.getWebappConfig().isUpdateSettingsFile()) {
if (!config.getWebappConfig().isUpdateSettingsFile()) {
webFilesManager.loadSettings();
webFilesManager.addFrom(configs.getWebappConfig());
webFilesManager.addFrom(config.getWebappConfig());
} else {
webFilesManager.setFrom(configs.getWebappConfig());
webFilesManager.setFrom(config.getWebappConfig());
}
for (String mapId : configs.getMapConfigs().keySet()) {
for (String mapId : config.getMapConfigs().keySet()) {
webFilesManager.addMap(mapId);
}
webFilesManager.saveSettings();
@ -180,32 +126,45 @@ public synchronized void createOrUpdateWebApp(boolean force) throws Configuratio
}
}
public synchronized Map<String, World> getWorlds() throws InterruptedException {
return getWorlds(mapId -> true);
/**
* Gets all loaded maps.
* @return A map of loaded maps
*/
public Map<String, BmMap> getMaps() {
return Collections.unmodifiableMap(maps);
}
public synchronized Map<String, World> getWorlds(Predicate<String> mapFilter) throws InterruptedException {
if (worlds == null) loadWorldsAndMaps(mapFilter);
return worlds;
/**
* Gets all loaded worlds.
* @return A map of loaded worlds
*/
public Map<String, World> getWorlds() {
return Collections.unmodifiableMap(worlds);
}
public synchronized Map<String, BmMap> getMaps() throws InterruptedException {
return getMaps(mapId -> true);
/**
* Gets or loads configured maps.
* @return A map of loaded maps
*/
public Map<String, BmMap> getOrLoadMaps() throws InterruptedException {
return getOrLoadMaps(mapId -> true);
}
public synchronized Map<String, BmMap> getMaps(Predicate<String> mapFilter) throws InterruptedException {
if (maps == null) loadWorldsAndMaps(mapFilter);
return maps;
}
/**
* Gets or loads configured maps.
* @param filter A predicate filtering map-ids that should be loaded
* (if maps are already loaded, they will be returned as well)
* @return A map of all loaded maps
*/
public synchronized Map<String, BmMap> getOrLoadMaps(Predicate<String> filter) throws InterruptedException {
for (var entry : config.getMapConfigs().entrySet()) {
if (Thread.interrupted()) throw new InterruptedException();
private synchronized void loadWorldsAndMaps(Predicate<String> mapFilter) throws InterruptedException {
maps = new HashMap<>();
worlds = new HashMap<>();
if (!filter.test(entry.getKey())) continue;
if (maps.containsKey(entry.getKey())) continue;
for (var entry : configs.getMapConfigs().entrySet()) {
if (!mapFilter.test(entry.getKey())) continue;
try {
loadMapConfig(entry.getKey(), entry.getValue());
loadMap(entry.getKey(), entry.getValue());
} catch (ConfigurationException ex) {
Logger.global.logWarning(ex.getFormattedExplanation());
Throwable cause = ex.getRootCause();
@ -214,16 +173,15 @@ private synchronized void loadWorldsAndMaps(Predicate<String> mapFilter) throws
}
}
}
worlds = Collections.unmodifiableMap(worlds);
maps = Collections.unmodifiableMap(maps);
return Collections.unmodifiableMap(maps);
}
private synchronized void loadMapConfig(String id, MapConfig mapConfig) throws ConfigurationException, InterruptedException {
private synchronized void loadMap(String id, MapConfig mapConfig) throws ConfigurationException, InterruptedException {
String name = mapConfig.getName();
if (name == null) name = id;
Path worldFolder = mapConfig.getWorld();
Key dimension = mapConfig.getDimension();
// if there is no world configured, we assume the map is static, or supplied from a different server
if (worldFolder == null) {
@ -231,48 +189,63 @@ private synchronized void loadMapConfig(String id, MapConfig mapConfig) throws C
return;
}
// if there is no dimension configured, we assume world-folder is actually the dimension-folder and convert (backwards compatibility)
if (dimension == null) {
worldFolder = worldFolder.normalize();
if (worldFolder.endsWith("DIM-1")) {
worldFolder = worldFolder.getParent();
dimension = DataPack.DIMENSION_THE_NETHER;
} else if (worldFolder.endsWith("DIM1")) {
worldFolder = worldFolder.getParent();
dimension = DataPack.DIMENSION_THE_END;
} else if (
worldFolder.getNameCount() > 3 &&
worldFolder.getName(worldFolder.getNameCount() - 3).equals(Path.of("dimensions"))
) {
String namespace = worldFolder.getName(worldFolder.getNameCount() - 2).toString();
String value = worldFolder.getName(worldFolder.getNameCount() - 1).toString();
worldFolder = worldFolder.subpath(0, worldFolder.getNameCount() - 3);
dimension = new Key(namespace, value);
} else {
dimension = DataPack.DIMENSION_OVERWORLD;
}
Logger.global.logInfo("The map '" + name + "' has no dimension configured.\n" +
"Assuming world: '" + worldFolder + "' and dimension: '" + dimension + "'.");
}
if (!Files.isDirectory(worldFolder)) {
throw new ConfigurationException(
"'" + worldFolder.toAbsolutePath().normalize() + "' does not exist or is no directory!\n" +
"Check if the 'world' setting in the config-file for that map is correct, or remove the entire config-file if you don't want that map.");
}
String worldId;
try {
worldId = getWorldId(worldFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"Could not load the ID for the world (" + worldFolder.toAbsolutePath().normalize() + ")!\n" +
"Make sure BlueMap has read and write access/permissions to the world-files for this map.",
ex);
}
String worldId = MCAWorld.id(worldFolder, dimension);
World world = worlds.get(worldId);
if (world == null) {
try {
Logger.global.logInfo("Loading world '" + worldId + "' (" + worldFolder.toAbsolutePath().normalize() + ")...");
world = new MCAWorld(worldFolder, mapConfig.getWorldSkyLight(), mapConfig.isIgnoreMissingLightData());
Logger.global.logDebug("Loading world " + worldId + " ...");
world = MCAWorld.load(worldFolder, dimension);
worlds.put(worldId, world);
} catch (IOException ex) {
throw new ConfigurationException(
"Failed to load world '" + worldId + "' (" + worldFolder.toAbsolutePath().normalize() + ")!\n" +
"Failed to load world " + worldId + "!\n" +
"Is the level.dat of that world present and not corrupted?",
ex);
}
}
Storage storage = getStorage(mapConfig.getStorage());
Storage storage = getOrLoadStorage(mapConfig.getStorage());
try {
Logger.global.logInfo("Loading map '" + name + "'...");
Logger.global.logInfo("Loading map '" + id + "'...");
BmMap map = new BmMap(
id,
name,
worldId,
world,
storage,
getResourcePack(),
getOrLoadResourcePack(),
mapConfig
);
maps.put(id, map);
@ -303,12 +276,12 @@ private synchronized void loadMapConfig(String id, MapConfig mapConfig) throws C
}
}
public synchronized Storage getStorage(String storageId) throws ConfigurationException {
public synchronized Storage getOrLoadStorage(String storageId) throws ConfigurationException, InterruptedException {
Storage storage = storages.get(storageId);
if (storage == null) {
try {
StorageConfig storageConfig = getConfigs().getStorageConfigs().get(storageId);
StorageConfig storageConfig = getConfig().getStorageConfigs().get(storageId);
if (storageConfig == null) {
throw new ConfigurationException("There is no storage-configuration for '" + storageId + "'!\n" +
"You will either need to define that storage, or change the map-config to use a storage-config that exists.");
@ -341,14 +314,18 @@ public synchronized Storage getStorage(String storageId) throws ConfigurationExc
return storage;
}
public synchronized ResourcePack getResourcePack() throws ConfigurationException, InterruptedException {
public @Nullable ResourcePack getResourcePack() {
return resourcePack;
}
public synchronized ResourcePack getOrLoadResourcePack() throws ConfigurationException, InterruptedException {
if (resourcePack == null) {
MinecraftVersion minecraftVersion = serverInterface.getMinecraftVersion();
MinecraftVersion minecraftVersion = config.getMinecraftVersion();
@Nullable Path resourcePackFolder = config.getResourcePacksFolder();
@Nullable Path modsFolder = config.getModsFolder();
Path defaultResourceFile = configs.getCoreConfig().getData().resolve("minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
Path resourceExtensionsFile = configs.getCoreConfig().getData().resolve("resourceExtensions.zip");
Path resourcePackFolder = serverInterface.getConfigFolder().resolve("resourcepacks");
Path defaultResourceFile = config.getCoreConfig().getData().resolve("minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
Path resourceExtensionsFile = config.getCoreConfig().getData().resolve("resourceExtensions.zip");
try {
FileHelper.createDirectories(resourcePackFolder);
@ -360,8 +337,10 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
ex);
}
if (Thread.interrupted()) throw new InterruptedException();
if (!Files.exists(defaultResourceFile)) {
if (configs.getCoreConfig().isAcceptDownload()) {
if (config.getCoreConfig().isAcceptDownload()) {
//download file
try {
Logger.global.logInfo("Downloading " + minecraftVersion.getResource().getClientUrl() + " to " + defaultResourceFile + " ...");
@ -380,6 +359,8 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
}
}
if (Thread.interrupted()) throw new InterruptedException();
try {
Files.deleteIfExists(resourceExtensionsFile);
FileHelper.createDirectories(resourceExtensionsFile.getParent());
@ -396,21 +377,25 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
ex);
}
if (Thread.interrupted()) throw new InterruptedException();
try {
resourcePack = new ResourcePack();
ResourcePack resourcePack = new ResourcePack();
List<Path> resourcePackRoots = new ArrayList<>();
if (resourcePackFolder != null) {
// load from resourcepack folder
try (Stream<Path> resourcepackFiles = Files.list(resourcePackFolder)) {
resourcepackFiles
.sorted(Comparator.reverseOrder())
.forEach(resourcePackRoots::add);
}
}
if (configs.getCoreConfig().isScanForModResources()) {
if (config.getCoreConfig().isScanForModResources()) {
// load from mods folder
Path modsFolder = serverInterface.getModsFolder().orElse(null);
if (modsFolder != null && Files.isDirectory(modsFolder)) {
try (Stream<Path> resourcepackFiles = Files.list(modsFolder)) {
resourcepackFiles
@ -436,23 +421,20 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
resourcePackRoots.add(defaultResourceFile);
resourcePack.loadResources(resourcePackRoots);
this.resourcePack = resourcePack;
} catch (IOException | RuntimeException e) {
throw new ConfigurationException("Failed to parse resources!\n" +
"Is one of your resource-packs corrupted?", e);
}
}
return resourcePack;
}
public Optional<ResourcePack> getResourcePackIfLoaded() {
return Optional.ofNullable(this.resourcePack);
return this.resourcePack;
}
private Collection<Path> getWorldFolders() {
Set<Path> folders = new HashSet<>();
for (MapConfig mapConfig : configs.getMapConfigs().values()) {
for (MapConfig mapConfig : config.getMapConfigs().values()) {
Path folder = mapConfig.getWorld();
if (folder == null) continue;
folder = folder.toAbsolutePath().normalize();
@ -463,8 +445,8 @@ private Collection<Path> getWorldFolders() {
return folders;
}
public BlueMapConfigProvider getConfigs() {
return configs;
public BlueMapConfiguration getConfig() {
return config;
}
@Override

View File

@ -24,6 +24,9 @@
*/
package de.bluecolored.bluemap.common;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.bluecolored.bluemap.common.config.WebappConfig;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
@ -47,6 +50,11 @@
public class WebFilesManager {
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
//.setPrettyPrinting() // enable pretty printing for easy editing
.create();
private final Path webRoot;
private Settings settings;
@ -61,7 +69,7 @@ public Path getSettingsFile() {
public void loadSettings() throws IOException {
try (BufferedReader reader = Files.newBufferedReader(getSettingsFile())) {
this.settings = ResourcesGson.INSTANCE.fromJson(reader, Settings.class);
this.settings = GSON.fromJson(reader, Settings.class);
}
}
@ -69,7 +77,7 @@ public void saveSettings() throws IOException {
FileHelper.createDirectories(getSettingsFile().getParent());
try (BufferedWriter writer = Files.newBufferedWriter(getSettingsFile(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
ResourcesGson.INSTANCE.toJson(this.settings, writer);
GSON.toJson(this.settings, writer);
}
}

View File

@ -30,13 +30,15 @@
import de.bluecolored.bluemap.api.BlueMapMap;
import de.bluecolored.bluemap.api.BlueMapWorld;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.World;
import java.io.IOException;
import java.util.*;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@ -75,30 +77,21 @@ public de.bluecolored.bluemap.api.plugin.Plugin getPlugin() {
@Override
public Collection<BlueMapMap> getMaps() {
Map<String, BmMap> maps = plugin.getMaps();
if (maps == null) return Collections.emptyList();
return maps.values().stream()
.map(map -> {
try {
return new BlueMapMapImpl(plugin, map);
} catch (IOException e) {
Logger.global.logError("[API] Failed to create BlueMapMap for map " + map.getId(), e);
return null;
}
})
.filter(Objects::nonNull)
Map<String, BmMap> maps = plugin.getBlueMap().getMaps();
return maps.keySet().stream()
.map(this::getMap)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toUnmodifiableSet());
}
@Override
public Collection<BlueMapWorld> getWorlds() {
Map<String, World> worlds = plugin.getWorlds();
if (worlds == null) return Collections.emptyList();
return worlds.values().stream()
.map(world -> getWorld(world).orElse(null))
.filter(Objects::nonNull)
Map<String, World> worlds = plugin.getBlueMap().getWorlds();
return worlds.keySet().stream()
.map(this::getWorld)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toUnmodifiableSet());
}
@ -108,43 +101,24 @@ public Optional<BlueMapWorld> getWorld(Object world) {
}
public Optional<BlueMapWorld> getWorldUncached(Object world) {
var worlds = plugin.getWorlds();
if (worlds == null) return Optional.empty();
if (world instanceof UUID) {
var coreWorld = worlds.get(world.toString());
if (coreWorld != null) world = coreWorld;
}
if (world instanceof String) {
var coreWorld = worlds.get(world);
var coreWorld = plugin.getBlueMap().getWorlds().get(world);
if (coreWorld != null) world = coreWorld;
}
if (world instanceof World) {
var coreWorld = (World) world;
try {
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
} catch (IOException e) {
Logger.global.logError("[API] Failed to create BlueMapWorld for world " + coreWorld.getSaveFolder(), e);
}
return Optional.empty();
}
var serverWorld = plugin.getServerInterface().getWorld(world).orElse(null);
ServerWorld serverWorld = plugin.getServerInterface().getServerWorld(world).orElse(null);
if (serverWorld == null) return Optional.empty();
try {
String id = plugin.getBlueMap().getWorldId(serverWorld.getSaveFolder());
var coreWorld = worlds.get(id);
World coreWorld = plugin.getWorld(serverWorld);
if (coreWorld == null) return Optional.empty();
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
} catch (IOException e) {
Logger.global.logError("[API] Failed to create BlueMapWorld for world " + serverWorld.getSaveFolder(), e);
return Optional.empty();
}
}
@Override
@ -153,8 +127,7 @@ public Optional<BlueMapMap> getMap(String id) {
}
public Optional<BlueMapMap> getMapUncached(String id) {
var maps = plugin.getMaps();
if (maps == null) return Optional.empty();
var maps = plugin.getBlueMap().getMaps();
var map = maps.get(id);
if (map == null) return Optional.empty();

View File

@ -28,8 +28,8 @@
import de.bluecolored.bluemap.api.BlueMapWorld;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.file.Path;
import java.util.Collection;
@ -38,13 +38,13 @@
public class BlueMapWorldImpl implements BlueMapWorld {
private final WeakReference<Plugin> plugin;
private final String id;
private final WeakReference<Plugin> plugin;
private final WeakReference<World> world;
public BlueMapWorldImpl(Plugin plugin, World world) throws IOException {
public BlueMapWorldImpl(Plugin plugin, World world) {
this.id = world.getId();
this.plugin = new WeakReference<>(plugin);
this.id = plugin.getBlueMap().getWorldId(world.getSaveFolder());
this.world = new WeakReference<>(world);
}
@ -58,15 +58,23 @@ public String getId() {
}
@Override
@Deprecated
public Path getSaveFolder() {
return unpack(world).getSaveFolder();
World world = unpack(this.world);
if (world instanceof MCAWorld) {
return ((MCAWorld) world).getDimensionFolder();
} else {
throw new UnsupportedOperationException("This world-type has no save-folder.");
}
}
@Override
public Collection<BlueMapMap> getMaps() {
return unpack(plugin).getMaps().values().stream()
.filter(map -> map.getWorld().equals(unpack(world)))
.map(map -> new BlueMapMapImpl(unpack(plugin), map, this))
Plugin plugin = unpack(this.plugin);
World world = unpack(this.world);
return plugin.getBlueMap().getMaps().values().stream()
.filter(map -> map.getWorld().equals(world))
.map(map -> new BlueMapMapImpl(plugin, map, this))
.collect(Collectors.toUnmodifiableSet());
}

View File

@ -82,7 +82,7 @@ public boolean isRunning() {
@Override
public void start() {
if (!isRunning()){
renderManager.start(plugin.getConfigs().getCoreConfig().getRenderThreadCount());
renderManager.start(plugin.getBlueMap().getConfig().getCoreConfig().getRenderThreadCount());
}
plugin.getPluginState().setRenderThreadsEnabled(true);
}

View File

@ -49,7 +49,7 @@ public WebAppImpl(Plugin plugin) {
@Override
public Path getWebRoot() {
return plugin.getConfigs().getWebappConfig().getWebroot();
return plugin.getBlueMap().getConfig().getWebappConfig().getWebroot();
}
@Override
@ -98,6 +98,7 @@ public String createImage(BufferedImage image, String path) throws IOException {
}
@Override
@Deprecated(forRemoval = true)
public Map<String, String> availableImages() throws IOException {
Path webRoot = getWebRoot().toAbsolutePath();
String separator = webRoot.getFileSystem().getSeparator();

View File

@ -25,114 +25,92 @@
package de.bluecolored.bluemap.common.config;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.BlueMapConfigProvider;
import de.bluecolored.bluemap.common.BlueMapConfiguration;
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.datapack.DataPack;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.util.Key;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.*;
import java.util.stream.Stream;
@DebugDump
public class BlueMapConfigs implements BlueMapConfigProvider {
@Getter
public class BlueMapConfigManager implements BlueMapConfiguration {
private final ServerInterface serverInterface;
private final ConfigManager configManager;
private final MinecraftVersion minecraftVersion;
private final CoreConfig coreConfig;
private final WebserverConfig webserverConfig;
private final WebappConfig webappConfig;
private final PluginConfig pluginConfig;
private final Map<String, MapConfig> mapConfigs;
private final Map<String, StorageConfig> storageConfigs;
private final Path resourcePacksFolder;
private final @Nullable Path modsFolder;
public BlueMapConfigs(ServerInterface serverInterface) throws ConfigurationException {
this(serverInterface, Path.of("bluemap"), Path.of("bluemap", "web"), true);
}
@Builder
private BlueMapConfigManager(
@NonNull MinecraftVersion minecraftVersion,
@NonNull Path configRoot,
@Nullable Path defaultDataFolder,
@Nullable Path defaultWebroot,
@Nullable Collection<ServerWorld> autoConfigWorlds,
@Nullable Boolean usePluginConfig,
@Nullable Boolean useMetricsConfig,
@Nullable Path resourcePacksFolder,
@Nullable Path modsFolder
) throws ConfigurationException {
// set defaults
if (defaultDataFolder == null) defaultDataFolder = Path.of("bluemap");
if (defaultWebroot == null) defaultWebroot = Path.of("bluemap", "web");
if (autoConfigWorlds == null) autoConfigWorlds = Collections.emptyList();
if (usePluginConfig == null) usePluginConfig = true;
if (useMetricsConfig == null) useMetricsConfig = true;
if (resourcePacksFolder == null) resourcePacksFolder = configRoot.resolve("resourcepacks");
public BlueMapConfigs(ServerInterface serverInterface, Path defaultDataFolder, Path defaultWebroot, boolean usePluginConf) throws ConfigurationException {
this.serverInterface = serverInterface;
this.configManager = new ConfigManager(serverInterface.getConfigFolder());
this.coreConfig = loadCoreConfig(defaultDataFolder);
// load
this.minecraftVersion = minecraftVersion;
this.configManager = new ConfigManager(configRoot);
this.coreConfig = loadCoreConfig(defaultDataFolder, useMetricsConfig);
this.webappConfig = loadWebappConfig(defaultWebroot);
this.webserverConfig = loadWebserverConfig(webappConfig.getWebroot(), coreConfig.getData());
this.pluginConfig = usePluginConf ? loadPluginConfig() : new PluginConfig();
this.pluginConfig = usePluginConfig ? loadPluginConfig() : new PluginConfig();
this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs(webappConfig.getWebroot()));
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs());
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs(autoConfigWorlds));
this.resourcePacksFolder = resourcePacksFolder;
this.modsFolder = modsFolder;
}
public ConfigManager getConfigManager() {
return configManager;
}
@Override
public CoreConfig getCoreConfig() {
return coreConfig;
}
@Override
public WebappConfig getWebappConfig() {
return webappConfig;
}
@Override
public WebserverConfig getWebserverConfig() {
return webserverConfig;
}
@Override
public PluginConfig getPluginConfig() {
return pluginConfig;
}
@Override
public Map<String, MapConfig> getMapConfigs() {
return mapConfigs;
}
@Override
public Map<String, StorageConfig> getStorageConfigs() {
return storageConfigs;
}
private synchronized CoreConfig loadCoreConfig(Path defaultDataFolder) throws ConfigurationException {
private CoreConfig loadCoreConfig(Path defaultDataFolder, boolean useMetricsConfig) throws ConfigurationException {
Path configFileRaw = Path.of("core");
Path configFile = configManager.findConfigPath(configFileRaw);
Path configFolder = configFile.getParent();
if (!Files.exists(configFile)) {
// determine render-thread preset (very pessimistic, rather let people increase it themselves)
Runtime runtime = Runtime.getRuntime();
int availableCores = runtime.availableProcessors();
long availableMemoryMiB = runtime.maxMemory() / 1024L / 1024L;
int presetRenderThreadCount = 1;
if (availableCores >= 6 && availableMemoryMiB >= 4096)
presetRenderThreadCount = 2;
if (availableCores >= 10 && availableMemoryMiB >= 8192)
presetRenderThreadCount = 3;
try {
FileHelper.createDirectories(configFolder);
Files.writeString(
configFolder.resolve("core.conf"),
configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/core.conf")
.setConditional("metrics", serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
.setConditional("metrics", useMetricsConfig)
.setVariable("timestamp", LocalDateTime.now().withNano(0).toString())
.setVariable("version", BlueMap.VERSION)
.setVariable("data", formatPath(defaultDataFolder))
.setVariable("implementation", "bukkit")
.setVariable("render-thread-count", Integer.toString(presetRenderThreadCount))
.setVariable("render-thread-count", Integer.toString(suggestRenderThreadCount()))
.setVariable("logfile", formatPath(defaultDataFolder.resolve("logs").resolve("debug.log")))
.setVariable("logfile-with-time", formatPath(defaultDataFolder.resolve("logs").resolve("debug_%1$tF_%1$tT.log")))
.build(),
@ -146,7 +124,22 @@ private synchronized CoreConfig loadCoreConfig(Path defaultDataFolder) throws Co
return configManager.loadConfig(configFileRaw, CoreConfig.class);
}
private synchronized WebserverConfig loadWebserverConfig(Path defaultWebroot, Path dataRoot) throws ConfigurationException {
/**
* determine render-thread preset (very pessimistic, rather let people increase it themselves)
*/
private int suggestRenderThreadCount() {
Runtime runtime = Runtime.getRuntime();
int availableCores = runtime.availableProcessors();
long availableMemoryMiB = runtime.maxMemory() / 1024L / 1024L;
int presetRenderThreadCount = 1;
if (availableCores >= 6 && availableMemoryMiB >= 4096)
presetRenderThreadCount = 2;
if (availableCores >= 10 && availableMemoryMiB >= 8192)
presetRenderThreadCount = 3;
return presetRenderThreadCount;
}
private WebserverConfig loadWebserverConfig(Path defaultWebroot, Path dataRoot) throws ConfigurationException {
Path configFileRaw = Path.of("webserver");
Path configFile = configManager.findConfigPath(configFileRaw);
Path configFolder = configFile.getParent();
@ -171,7 +164,7 @@ private synchronized WebserverConfig loadWebserverConfig(Path defaultWebroot, Pa
return configManager.loadConfig(configFileRaw, WebserverConfig.class);
}
private synchronized WebappConfig loadWebappConfig(Path defaultWebroot) throws ConfigurationException {
private WebappConfig loadWebappConfig(Path defaultWebroot) throws ConfigurationException {
Path configFileRaw = Path.of("webapp");
Path configFile = configManager.findConfigPath(configFileRaw);
Path configFolder = configFile.getParent();
@ -194,7 +187,7 @@ private synchronized WebappConfig loadWebappConfig(Path defaultWebroot) throws C
return configManager.loadConfig(configFileRaw, WebappConfig.class);
}
private synchronized PluginConfig loadPluginConfig() throws ConfigurationException {
private PluginConfig loadPluginConfig() throws ConfigurationException {
Path configFileRaw = Path.of("plugin");
Path configFile = configManager.findConfigPath(configFileRaw);
Path configFolder = configFile.getParent();
@ -216,7 +209,7 @@ private synchronized PluginConfig loadPluginConfig() throws ConfigurationExcepti
return configManager.loadConfig(configFileRaw, PluginConfig.class);
}
private synchronized Map<String, MapConfig> loadMapConfigs() throws ConfigurationException {
private Map<String, MapConfig> loadMapConfigs(Collection<ServerWorld> autoConfigWorlds) throws ConfigurationException {
Map<String, MapConfig> mapConfigs = new HashMap<>();
Path mapFolder = Paths.get("maps");
@ -225,41 +218,68 @@ private synchronized Map<String, MapConfig> loadMapConfigs() throws Configuratio
if (!Files.exists(mapConfigFolder)){
try {
FileHelper.createDirectories(mapConfigFolder);
var worlds = serverInterface.getLoadedWorlds();
if (worlds.isEmpty()) {
if (autoConfigWorlds.isEmpty()) {
Path worldFolder = Path.of("world");
Files.writeString(
mapConfigFolder.resolve("overworld.conf"),
createOverworldMapTemplate("Overworld", Path.of("world"), 0).build(),
createOverworldMapTemplate("Overworld", worldFolder,
DataPack.DIMENSION_OVERWORLD, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
Files.writeString(
mapConfigFolder.resolve("nether.conf"),
createNetherMapTemplate("Nether", Path.of("world", "DIM-1"), 0).build(),
createNetherMapTemplate("Nether", worldFolder,
DataPack.DIMENSION_THE_NETHER, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
Files.writeString(
mapConfigFolder.resolve("end.conf"),
createEndMapTemplate("End", Path.of("world", "DIM1"), 0).build(),
createEndMapTemplate("End", worldFolder,
DataPack.DIMENSION_THE_END, 0).build(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING
);
} else {
for (var world : worlds) {
String name = world.getName().orElse(world.getDimension().getName());
Path worldFolder = world.getSaveFolder();
// make sure overworld-dimensions come first, so they are the ones where the
// dimension-key is omitted in the generated map-id
List<ServerWorld> overworldFirstAutoConfigWorlds = new ArrayList<>(autoConfigWorlds.size());
overworldFirstAutoConfigWorlds.addAll(autoConfigWorlds);
overworldFirstAutoConfigWorlds.sort(Comparator.comparingInt(w ->
DataPack.DIMENSION_OVERWORLD.equals(w.getDimension()) ? 0 : 1
));
Path configFile = mapConfigFolder.resolve(sanitiseMapId(name.toLowerCase(Locale.ROOT)) + ".conf");
Set<String> mapIds = new HashSet<>();
for (var world : overworldFirstAutoConfigWorlds) {
Path worldFolder = world.getWorldFolder().normalize();
Key dimension = world.getDimension();
String dimensionName = dimension.getNamespace().equals("minecraft") ?
dimension.getValue() : dimension.getFormatted();
// find unique map id
String id = sanitiseMapId(worldFolder.getFileName().toString()).toLowerCase(Locale.ROOT);
if (mapIds.contains(id))
id = sanitiseMapId(worldFolder.getFileName() + "_" + dimensionName).toLowerCase(Locale.ROOT);
int i = 1;
while (Files.exists(configFile)) {
configFile = mapConfigFolder.resolve(sanitiseMapId(name.toLowerCase(Locale.ROOT)) + '_' + (++i) + ".conf");
}
String uniqueId = id;
while (mapIds.contains(uniqueId))
uniqueId = id + "_" + (++i);
mapIds.add(uniqueId);
if (i > 1) name = name + " " + i;
Path configFile = mapConfigFolder.resolve(uniqueId + ".conf");
String name = worldFolder.getFileName() + " (" + dimensionName + ")";
if (i > 1) name = name + " (" + i + ")";
ConfigTemplate template;
switch (world.getDimension()) {
case NETHER: template = createNetherMapTemplate(name, worldFolder, i - 1); break;
case END: template = createEndMapTemplate(name, worldFolder, i - 1); break;
default: template = createOverworldMapTemplate(name, worldFolder, i - 1); break;
switch (world.getDimension().getFormatted()) {
case "minecraft:the_nether":
template = createNetherMapTemplate(name, worldFolder, dimension, i - 1);
break;
case "minecraft:the_end":
template = createEndMapTemplate(name, worldFolder, dimension, i - 1);
break;
default:
template = createOverworldMapTemplate(name, worldFolder, dimension, i - 1);
break;
}
Files.writeString(
@ -302,7 +322,7 @@ private synchronized Map<String, MapConfig> loadMapConfigs() throws Configuratio
return mapConfigs;
}
private synchronized Map<String, StorageConfig> loadStorageConfigs(Path defaultWebroot) throws ConfigurationException {
private Map<String, StorageConfig> loadStorageConfigs(Path defaultWebroot) throws ConfigurationException {
Map<String, StorageConfig> storageConfigs = new HashMap<>();
Path storageFolder = Paths.get("storages");
@ -357,43 +377,43 @@ private String sanitiseMapId(String id) {
return id.replaceAll("\\W", "_");
}
private ConfigTemplate createOverworldMapTemplate(String name, Path worldFolder, int index) throws IOException {
private ConfigTemplate createOverworldMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
.setVariable("name", name)
.setVariable("sorting", "" + index)
.setVariable("world", formatPath(worldFolder))
.setVariable("dimension", dimension.getFormatted())
.setVariable("sky-color", "#7dabff")
.setVariable("void-color", "#000000")
.setVariable("ambient-light", "0.1")
.setVariable("world-sky-light", "15")
.setVariable("remove-caves-below-y", "55")
.setConditional("max-y-comment", true)
.setVariable("max-y", "100");
}
private ConfigTemplate createNetherMapTemplate(String name, Path worldFolder, int index) throws IOException {
private ConfigTemplate createNetherMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
.setVariable("name", name)
.setVariable("sorting", "" + (100 + index))
.setVariable("world", formatPath(worldFolder))
.setVariable("dimension", dimension.getFormatted())
.setVariable("sky-color", "#290000")
.setVariable("void-color", "#150000")
.setVariable("ambient-light", "0.6")
.setVariable("world-sky-light", "0")
.setVariable("remove-caves-below-y", "-10000")
.setConditional("max-y-comment", false)
.setVariable("max-y", "90");
}
private ConfigTemplate createEndMapTemplate(String name, Path worldFolder, int index) throws IOException {
private ConfigTemplate createEndMapTemplate(String name, Path worldFolder, Key dimension, int index) throws IOException {
return configManager.loadConfigTemplate("/de/bluecolored/bluemap/config/maps/map.conf")
.setVariable("name", name)
.setVariable("sorting", "" + (200 + index))
.setVariable("world", formatPath(worldFolder))
.setVariable("dimension", dimension.getFormatted())
.setVariable("sky-color", "#080010")
.setVariable("void-color", "#080010")
.setVariable("ambient-light", "0.6")
.setVariable("world-sky-light", "0")
.setVariable("remove-caves-below-y", "-10000")
.setConditional("max-y-comment", true)
.setVariable("max-y", "100");

View File

@ -25,8 +25,10 @@
package de.bluecolored.bluemap.common.config;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.common.config.typeserializer.KeyTypeSerializer;
import de.bluecolored.bluemap.common.config.typeserializer.Vector2iTypeSerializer;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.util.Key;
import org.apache.commons.io.IOUtils;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
@ -165,6 +167,7 @@ private ConfigurationLoader<? extends ConfigurationNode> getLoader(Path path){
.path(path)
.defaultOptions(o -> o.serializers(b -> {
b.register(Vector2i.class, new Vector2iTypeSerializer());
b.register(Key.class, new KeyTypeSerializer());
}))
.build();
}

View File

@ -28,43 +28,45 @@
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.map.MapSettings;
import de.bluecolored.bluemap.core.util.Key;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import java.nio.file.Path;
import java.util.Optional;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@DebugDump
@ConfigSerializable
@Getter
public class MapConfig implements MapSettings {
private String name = null;
@Nullable private Path world = null;
@Nullable private Key dimension = null;
private Path world = null;
@Nullable private String name = null;
private int sorting = 0;
private Vector2i startPos = null;
@Nullable private Vector2i startPos = null;
private String skyColor = "#7dabff";
private String voidColor = "#000000";
private float ambientLight = 0;
private int worldSkyLight = 15;
private int removeCavesBelowY = 55;
private int caveDetectionOceanFloor = 10000;
private boolean caveDetectionUsesBlockLight = false;
private int minX = Integer.MIN_VALUE;
private int maxX = Integer.MAX_VALUE;
private int minZ = Integer.MIN_VALUE;
private int maxZ = Integer.MAX_VALUE;
private int minY = Integer.MIN_VALUE;
private int maxY = Integer.MAX_VALUE;
@Getter(AccessLevel.NONE) private int minX = Integer.MIN_VALUE;
@Getter(AccessLevel.NONE) private int maxX = Integer.MAX_VALUE;
@Getter(AccessLevel.NONE) private int minZ = Integer.MIN_VALUE;
@Getter(AccessLevel.NONE) private int maxZ = Integer.MAX_VALUE;
@Getter(AccessLevel.NONE) private int minY = Integer.MIN_VALUE;
@Getter(AccessLevel.NONE) private int maxY = Integer.MAX_VALUE;
private transient Vector3i min = null;
private transient Vector3i max = null;
@ -80,7 +82,7 @@ public class MapConfig implements MapSettings {
private boolean ignoreMissingLightData = false;
private ConfigurationNode markerSets = null;
@Nullable private ConfigurationNode markerSets = null;
// hidden config fields
private int hiresTileSize = 32;
@ -88,61 +90,6 @@ public class MapConfig implements MapSettings {
private int lodCount = 3;
private int lodFactor = 5;
@Nullable
public String getName() {
return name;
}
@Nullable
public Path getWorld() {
return world;
}
@Override
public int getSorting() {
return sorting;
}
@Override
public Optional<Vector2i> getStartPos() {
return Optional.ofNullable(startPos);
}
@Override
public String getSkyColor() {
return skyColor;
}
@Override
public String getVoidColor() {
return voidColor;
}
@Override
public float getAmbientLight() {
return ambientLight;
}
@Override
public int getWorldSkyLight() {
return worldSkyLight;
}
@Override
public int getRemoveCavesBelowY() {
return removeCavesBelowY;
}
@Override
public boolean isCaveDetectionUsesBlockLight() {
return caveDetectionUsesBlockLight;
}
@Override
public int getCaveDetectionOceanFloor() {
return caveDetectionOceanFloor;
}
public Vector3i getMinPos() {
if (min == null) min = new Vector3i(minX, minY, minZ);
return min;
@ -153,57 +100,4 @@ public Vector3i getMaxPos() {
return max;
}
@Override
public long getMinInhabitedTime() {
return minInhabitedTime;
}
@Override
public int getMinInhabitedTimeRadius() {
return minInhabitedTimeRadius;
}
@Override
public boolean isRenderEdges() {
return renderEdges;
}
@Override
public boolean isSaveHiresLayer() {
return saveHiresLayer;
}
public String getStorage() {
return storage;
}
public boolean isIgnoreMissingLightData() {
return ignoreMissingLightData;
}
@Nullable
public ConfigurationNode getMarkerSets() {
return markerSets;
}
@Override
public int getHiresTileSize() {
return hiresTileSize;
}
@Override
public int getLowresTileSize() {
return lowresTileSize;
}
@Override
public int getLodCount() {
return lodCount;
}
@Override
public int getLodFactor() {
return lodFactor;
}
}

View File

@ -22,30 +22,27 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.serverinterface;
package de.bluecolored.bluemap.common.config.typeserializer;
import java.nio.file.Path;
import de.bluecolored.bluemap.core.util.Key;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
public enum Dimension {
import java.lang.reflect.Type;
OVERWORLD ("Overworld", Path.of("")),
NETHER ("Nether", Path.of("DIM-1")),
END ("End", Path.of("DIM1"));
public class KeyTypeSerializer implements TypeSerializer<Key> {
private final String name;
private final Path dimensionSubPath;
Dimension(String name, Path dimensionSubPath) {
this.name = name;
this.dimensionSubPath = dimensionSubPath;
@Override
public Key deserialize(Type type, ConfigurationNode node) {
String formatted = node.getString();
return formatted != null ? new Key(node.getString()) : null;
}
public String getName() {
return name;
}
public Path getDimensionSubPath() {
return dimensionSubPath;
@Override
public void serialize(Type type, @Nullable Key obj, ConfigurationNode node) throws SerializationException {
if (obj != null) node.set(obj.getFormatted());
}
}

View File

@ -27,9 +27,9 @@
import com.google.gson.stream.JsonWriter;
import de.bluecolored.bluemap.common.config.PluginConfig;
import de.bluecolored.bluemap.common.serverinterface.Player;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.logger.Logger;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.StringWriter;
@ -39,15 +39,15 @@
public class LivePlayersDataSupplier implements Supplier<String> {
private final ServerInterface server;
private final Server server;
private final PluginConfig config;
@Nullable private final String worldId;
private final ServerWorld world;
private final Predicate<UUID> playerFilter;
public LivePlayersDataSupplier(ServerInterface server, PluginConfig config, @Nullable String worldId, Predicate<UUID> playerFilter) {
public LivePlayersDataSupplier(Server server, PluginConfig config, ServerWorld world, Predicate<UUID> playerFilter) {
this.server = server;
this.config = config;
this.worldId = worldId;
this.world = world;
this.playerFilter = playerFilter;
}
@ -61,9 +61,7 @@ public String get() {
if (config.isLivePlayerMarkers()) {
for (Player player : this.server.getOnlinePlayers()) {
if (!player.isOnline()) continue;
boolean isCorrectWorld = player.getWorld().equals(this.worldId);
boolean isCorrectWorld = player.getWorld().equals(this.world);
if (config.isHideInvisible() && player.isInvisible()) continue;
if (config.isHideVanished() && player.isVanished()) continue;

View File

@ -25,7 +25,7 @@
package de.bluecolored.bluemap.common.plugin;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.BlueMapConfigProvider;
import de.bluecolored.bluemap.common.BlueMapConfiguration;
import de.bluecolored.bluemap.common.BlueMapService;
import de.bluecolored.bluemap.common.InterruptableReentrantLock;
import de.bluecolored.bluemap.common.MissingResourcesException;
@ -36,7 +36,8 @@
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.common.web.*;
import de.bluecolored.bluemap.common.web.http.HttpServer;
import de.bluecolored.bluemap.core.debug.StateDumper;
@ -46,7 +47,9 @@
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.configurate.gson.GsonConfigurationLoader;
import org.spongepowered.configurate.serialize.SerializationException;
@ -78,15 +81,12 @@ public class Plugin implements ServerEventListener {
private final InterruptableReentrantLock loadingLock = new InterruptableReentrantLock();
private final String implementationType;
private final ServerInterface serverInterface;
private final Server serverInterface;
private BlueMapService blueMap;
private PluginState pluginState;
private Map<String, World> worlds;
private Map<String, BmMap> maps;
private RenderManager renderManager;
private HttpServer webServer;
private Logger webLogger;
@ -101,7 +101,7 @@ public class Plugin implements ServerEventListener {
private boolean loaded = false;
public Plugin(String implementationType, ServerInterface serverInterface) {
public Plugin(String implementationType, Server serverInterface) {
this.implementationType = implementationType.toLowerCase();
this.serverInterface = serverInterface;
@ -121,11 +121,18 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
unload(); //ensure nothing is left running (from a failed load or something)
//load configs
blueMap = new BlueMapService(serverInterface, new BlueMapConfigs(serverInterface), preloadedResourcePack);
CoreConfig coreConfig = getConfigs().getCoreConfig();
WebserverConfig webserverConfig = getConfigs().getWebserverConfig();
WebappConfig webappConfig = getConfigs().getWebappConfig();
PluginConfig pluginConfig = getConfigs().getPluginConfig();
BlueMapConfigManager configManager = BlueMapConfigManager.builder()
.minecraftVersion(serverInterface.getMinecraftVersion())
.configRoot(serverInterface.getConfigFolder())
.resourcePacksFolder(serverInterface.getConfigFolder().resolve("resourcepacks"))
.modsFolder(serverInterface.getModsFolder().orElse(null))
.useMetricsConfig(serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
.autoConfigWorlds(serverInterface.getLoadedServerWorlds())
.build();
CoreConfig coreConfig = configManager.getCoreConfig();
WebserverConfig webserverConfig = configManager.getWebserverConfig();
WebappConfig webappConfig = configManager.getWebappConfig();
PluginConfig pluginConfig = configManager.getPluginConfig();
//apply new file-logger config
if (coreConfig.getLog().getFile() != null) {
@ -149,16 +156,19 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
pluginState = new PluginState();
}
//create bluemap-service
blueMap = new BlueMapService(configManager, preloadedResourcePack);
//try load resources
try {
blueMap.getResourcePack();
blueMap.getOrLoadResourcePack();
} catch (MissingResourcesException ex) {
Logger.global.logWarning("BlueMap is missing important resources!");
Logger.global.logWarning("You must accept the required file download in order for BlueMap to work!");
BlueMapConfigProvider configProvider = blueMap.getConfigs();
if (configProvider instanceof BlueMapConfigs) {
Logger.global.logWarning("Please check: " + ((BlueMapConfigs) configProvider).getConfigManager().findConfigPath(Path.of("core")).toAbsolutePath().normalize());
BlueMapConfiguration configProvider = blueMap.getConfig();
if (configProvider instanceof BlueMapConfigManager) {
Logger.global.logWarning("Please check: " + ((BlueMapConfigManager) configProvider).getConfigManager().findConfigPath(Path.of("core")).toAbsolutePath().normalize());
}
Logger.global.logInfo("If you have changed the config you can simply reload the plugin using: /bluemap reload");
@ -167,9 +177,8 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
return;
}
//load worlds and maps
worlds = blueMap.getWorlds();
maps = blueMap.getMaps();
//load maps
Map<String, BmMap> maps = blueMap.getOrLoadMaps();
//create and start webserver
if (webserverConfig.isEnabled()) {
@ -182,7 +191,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
routingRequestHandler.register(".*", new FileRequestHandler(webroot));
// map route
for (var mapConfigEntry : getConfigs().getMapConfigs().entrySet()) {
for (var mapConfigEntry : configManager.getMapConfigs().entrySet()) {
String id = mapConfigEntry.getKey();
MapConfig mapConfig = mapConfigEntry.getValue();
@ -191,7 +200,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
if (map != null) {
mapRequestHandler = new MapRequestHandler(map, serverInterface, pluginConfig, Predicate.not(pluginState::isPlayerHidden));
} else {
Storage storage = blueMap.getStorage(mapConfig.getStorage());
Storage storage = blueMap.getOrLoadStorage(mapConfig.getStorage());
mapRequestHandler = new MapRequestHandler(id, storage);
}
@ -447,9 +456,6 @@ public void unload(boolean keepWebserver) {
Logger.global.remove(DEBUG_FILE_LOG_NAME);
//clear resources
worlds = null;
maps = null;
pluginState = null;
//done
@ -479,7 +485,7 @@ public void lightReload() throws IOException {
}
// hold and reuse loaded resourcepack
ResourcePack preloadedResourcePack = this.blueMap.getResourcePackIfLoaded().orElse(null);
ResourcePack preloadedResourcePack = this.blueMap.getResourcePack();
unload();
load(preloadedResourcePack);
@ -491,10 +497,12 @@ public void lightReload() throws IOException {
}
public synchronized void save() {
if (blueMap == null) return;
if (pluginState != null) {
try {
GsonConfigurationLoader loader = GsonConfigurationLoader.builder()
.path(blueMap.getConfigs().getCoreConfig().getData().resolve("pluginState.json"))
.path(blueMap.getConfig().getCoreConfig().getData().resolve("pluginState.json"))
.build();
loader.save(loader.createNode().set(PluginState.class, pluginState));
} catch (IOException ex) {
@ -502,28 +510,32 @@ public synchronized void save() {
}
}
if (maps != null) {
var maps = blueMap.getMaps();
for (BmMap map : maps.values()) {
map.save();
}
}
}
public void saveMarkerStates() {
if (maps != null) {
if (blueMap == null) return;
var maps = blueMap.getMaps();
for (BmMap map : maps.values()) {
map.saveMarkerState();
}
}
}
public void savePlayerStates() {
if (maps != null) {
if (blueMap == null) return;
var maps = blueMap.getMaps();
for (BmMap map : maps.values()) {
var serverWorld = serverInterface.getServerWorld(map.getWorld()).orElse(null);
if (serverWorld == null) continue;
var dataSupplier = new LivePlayersDataSupplier(
serverInterface,
getConfigs().getPluginConfig(),
map.getWorldId(),
getBlueMap().getConfig().getPluginConfig(),
serverWorld,
Predicate.not(pluginState::isPlayerHidden)
);
try (
@ -536,7 +548,6 @@ public void savePlayerStates() {
}
}
}
}
public synchronized void startWatchingMap(BmMap map) {
stopWatchingMap(map);
@ -558,7 +569,7 @@ public synchronized void stopWatchingMap(BmMap map) {
}
public boolean flushWorldUpdates(World world) throws IOException {
var implWorld = serverInterface.getWorld(world.getSaveFolder()).orElse(null);
var implWorld = serverInterface.getServerWorld(world).orElse(null);
if (implWorld != null) return implWorld.persistWorldChanges();
return false;
}
@ -588,8 +599,8 @@ public void run() {
}
public boolean checkPausedByPlayerCount() {
CoreConfig coreConfig = getConfigs().getCoreConfig();
PluginConfig pluginConfig = getConfigs().getPluginConfig();
CoreConfig coreConfig = getBlueMap().getConfig().getCoreConfig();
PluginConfig pluginConfig = getBlueMap().getConfig().getPluginConfig();
if (
pluginConfig.getPlayerRenderLimit() > 0 &&
@ -604,7 +615,12 @@ public boolean checkPausedByPlayerCount() {
}
}
public ServerInterface getServerInterface() {
public @Nullable World getWorld(ServerWorld serverWorld) {
String id = MCAWorld.id(serverWorld.getWorldFolder(), serverWorld.getDimension());
return getBlueMap().getWorlds().get(id);
}
public Server getServerInterface() {
return serverInterface;
}
@ -612,22 +628,10 @@ public BlueMapService getBlueMap() {
return blueMap;
}
public BlueMapConfigProvider getConfigs() {
return blueMap.getConfigs();
}
public PluginState getPluginState() {
return pluginState;
}
public Map<String, World> getWorlds(){
return worlds;
}
public Map<String, BmMap> getMaps(){
return maps;
}
public RenderManager getRenderManager() {
return renderManager;
}
@ -649,11 +653,14 @@ public PlayerSkinUpdater getSkinUpdater() {
}
private void initFileWatcherTasks() {
var maps = blueMap.getMaps();
if (maps != null) {
for (BmMap map : maps.values()) {
if (pluginState.getMapState(map).isUpdateEnabled()) {
startWatchingMap(map);
}
}
}
}
}

View File

@ -25,12 +25,14 @@
package de.bluecolored.bluemap.common.plugin;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import java.io.IOException;
import java.nio.file.*;
@ -45,7 +47,7 @@ public class RegionFileWatchService extends Thread {
private final RenderManager renderManager;
private final WatchService watchService;
private boolean verbose;
private final boolean verbose;
private volatile boolean closed;
private Timer delayTimer;
@ -60,7 +62,9 @@ public RegionFileWatchService(RenderManager renderManager, BmMap map, boolean ve
this.closed = false;
this.scheduledUpdates = new HashMap<>();
Path folder = map.getWorld().getSaveFolder().resolve("region");
World world = map.getWorld();
if (!(world instanceof MCAWorld)) throw new UnsupportedOperationException("world-type is not supported");
Path folder = ((MCAWorld) world).getRegionFolder();
FileHelper.createDirectories(folder);
this.watchService = folder.getFileSystem().newWatchService();

View File

@ -30,7 +30,6 @@
import de.bluecolored.bluemap.common.plugin.text.TextFormat;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.RenderTask;
import de.bluecolored.bluemap.core.world.World;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.lang.ref.WeakReference;
@ -106,7 +105,7 @@ public List<Text> createStatusMessage(){
if (plugin.checkPausedByPlayerCount()) {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ",
Text.of(TextColor.GOLD, "paused")));
lines.add(Text.of(TextColor.GRAY, TextFormat.ITALIC, "\u00A0\u00A0\u00A0(there are " + plugin.getConfigs().getPluginConfig().getPlayerRenderLimit() + " or more players online)"));
lines.add(Text.of(TextColor.GRAY, TextFormat.ITALIC, "\u00A0\u00A0\u00A0(there are " + plugin.getBlueMap().getConfig().getPluginConfig().getPlayerRenderLimit() + " or more players online)"));
} else {
lines.add(Text.of(TextColor.WHITE, " Render-Threads are ",
Text.of(TextColor.RED, "stopped")
@ -134,20 +133,22 @@ public List<Text> createStatusMessage(){
public Text worldHelperHover() {
StringJoiner joiner = new StringJoiner("\n");
for (World world : plugin.getWorlds().values()) {
joiner.add(world.getName());
for (String worldId : plugin.getBlueMap().getWorlds().keySet()) {
joiner.add(worldId);
}
return Text.of("world").setHoverText(Text.of(TextColor.WHITE, "Available worlds: \n", TextColor.GRAY, joiner.toString()));
return Text.of(TextFormat.UNDERLINED, "world")
.setHoverText(Text.of(TextColor.WHITE, "Available worlds: \n", TextColor.GRAY, joiner.toString()));
}
public Text mapHelperHover() {
StringJoiner joiner = new StringJoiner("\n");
for (String mapId : plugin.getMaps().keySet()) {
for (String mapId : plugin.getBlueMap().getMaps().keySet()) {
joiner.add(mapId);
}
return Text.of("map").setHoverText(Text.of(TextColor.WHITE, "Available maps: \n", TextColor.GRAY, joiner.toString()));
return Text.of(TextFormat.UNDERLINED, "map")
.setHoverText(Text.of(TextColor.WHITE, "Available maps: \n", TextColor.GRAY, joiner.toString()));
}
public synchronized Optional<RenderTask> getTaskForRef(String ref) {

View File

@ -53,7 +53,7 @@
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.map.MapRenderState;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.Block;
import de.bluecolored.bluemap.core.world.block.Block;
import de.bluecolored.bluemap.core.world.World;
import java.io.IOException;
@ -281,10 +281,10 @@ private <T> Optional<T> getOptionalArgument(CommandContext<S> context, String ar
}
}
private Optional<World> parseWorld(String worldName) {
for (World world : plugin.getWorlds().values()) {
if (world.getName().equalsIgnoreCase(worldName)) {
return Optional.of(world);
private Optional<World> parseWorld(String worldId) {
for (var entry : plugin.getBlueMap().getWorlds().entrySet()) {
if (entry.getKey().equals(worldId)) {
return Optional.of(entry.getValue());
}
}
@ -292,8 +292,8 @@ private Optional<World> parseWorld(String worldName) {
}
private Optional<BmMap> parseMap(String mapId) {
for (BmMap map : plugin.getMaps().values()) {
if (map.getId().equalsIgnoreCase(mapId)) {
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
if (map.getId().equals(mapId)) {
return Optional.of(map);
}
}
@ -416,7 +416,7 @@ public int reloadCommand(CommandContext<S> context, boolean light) {
public int debugClearCacheCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
for (World world : plugin.getWorlds().values()) {
for (World world : plugin.getBlueMap().getWorlds().values()) {
world.invalidateChunkCache();
}
@ -436,7 +436,7 @@ public int debugFlushCommand(CommandContext<S> context) {
world = parseWorld(worldName.get()).orElse(null);
if (world == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this name: ", TextColor.WHITE, worldName.get()));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this id: ", TextColor.WHITE, worldName.get()));
return 0;
}
} else {
@ -482,7 +482,7 @@ public int debugBlockCommand(CommandContext<S> context) {
position = new Vector3d(x.get(), y.get(), z.get());
if (world == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this name: ", TextColor.WHITE, worldName.get()));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " with this id: ", TextColor.WHITE, worldName.get()));
return 0;
}
} else {
@ -499,7 +499,7 @@ public int debugBlockCommand(CommandContext<S> context) {
// collect and output debug info
Vector3i blockPos = position.floor().toInt();
Block<?> block = new Block<>(world, blockPos.getX(), blockPos.getY(), blockPos.getZ());
Block<?> blockBelow = new Block<>(null, 0, 0, 0).copy(block, 0, -1, 0);
Block<?> blockBelow = new Block<>(world, blockPos.getX(), blockPos.getY() - 1, blockPos.getZ());
// populate lazy-loaded values
block.getBlockState();
@ -523,7 +523,7 @@ public int debugDumpCommand(CommandContext<S> context) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
try {
Path file = plugin.getConfigs().getCoreConfig().getData().resolve("dump.json");
Path file = plugin.getBlueMap().getConfig().getCoreConfig().getData().resolve("dump.json");
StateDumper.global().dump(file);
source.sendMessage(Text.of(TextColor.GREEN, "Dump created at: " + file));
@ -562,7 +562,7 @@ public int startCommand(CommandContext<S> context) {
new Thread(() -> {
plugin.getPluginState().setRenderThreadsEnabled(true);
plugin.getRenderManager().start(plugin.getConfigs().getCoreConfig().resolveRenderThreadCount());
plugin.getRenderManager().start(plugin.getBlueMap().getConfig().getCoreConfig().resolveRenderThreadCount());
source.sendMessage(Text.of(TextColor.GREEN, "Render-Threads started!"));
plugin.save();
@ -583,7 +583,7 @@ public int freezeCommand(CommandContext<S> context) {
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapString));
return 0;
}
@ -624,7 +624,7 @@ public int unfreezeCommand(CommandContext<S> context) {
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapString));
return 0;
}
@ -671,7 +671,8 @@ public int updateCommand(CommandContext<S> context, boolean force) {
mapToRender = parseMap(worldOrMap.get()).orElse(null);
if (mapToRender == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " or ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, worldOrMap.get()));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.worldHelperHover(), " or ",
helper.mapHelperHover(), " with this id: ", TextColor.WHITE, worldOrMap.get()));
return 0;
}
} else {
@ -682,7 +683,8 @@ public int updateCommand(CommandContext<S> context, boolean force) {
mapToRender = null;
if (worldToRender == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a world from this command-source, you'll have to define a world or a map to update!").setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " <world|map>")));
source.sendMessage(Text.of(TextColor.RED, "Can't detect a world from this command-source, you'll have to define a world or a map to update!")
.setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " <world|map>")));
return 0;
}
}
@ -699,7 +701,8 @@ public int updateCommand(CommandContext<S> context, boolean force) {
} else {
Vector3d position = source.getPosition().orElse(null);
if (position == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a position from this command-source, you'll have to define x,z coordinates to update with a radius!").setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " <x> <z> " + radius)));
source.sendMessage(Text.of(TextColor.RED, "Can't detect a position from this command-source, you'll have to define x,z coordinates to update with a radius!")
.setHoverText(Text.of(TextColor.GRAY, "/bluemap " + (force ? "force-update" : "update") + " <x> <z> " + radius)));
return 0;
}
@ -714,16 +717,12 @@ public int updateCommand(CommandContext<S> context, boolean force) {
try {
List<BmMap> maps = new ArrayList<>();
if (worldToRender != null) {
var world = plugin.getServerInterface().getWorld(worldToRender.getSaveFolder()).orElse(null);
if (world != null) world.persistWorldChanges();
for (BmMap map : plugin.getMaps().values()) {
if (map.getWorld().getSaveFolder().equals(worldToRender.getSaveFolder())) maps.add(map);
plugin.flushWorldUpdates(worldToRender);
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
if (map.getWorld().equals(worldToRender)) maps.add(map);
}
} else {
var world = plugin.getServerInterface().getWorld(mapToRender.getWorld().getSaveFolder()).orElse(null);
if (world != null) world.persistWorldChanges();
plugin.flushWorldUpdates(mapToRender.getWorld());
maps.add(mapToRender);
}
@ -741,7 +740,8 @@ public int updateCommand(CommandContext<S> context, boolean force) {
updateTask.getRegions().forEach(region -> state.setRenderTime(region, -1));
}
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "' ", TextColor.GRAY, "(" + updateTask.getRegions().size() + " regions, ~" + updateTask.getRegions().size() * 1024L + " chunks)"));
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "' ",
TextColor.GRAY, "(" + updateTask.getRegions().size() + " regions, ~" + updateTask.getRegions().size() * 1024L + " chunks)"));
}
source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress."));
@ -790,7 +790,7 @@ public int purgeCommand(CommandContext<S> context) {
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapString));
return 0;
}
@ -831,8 +831,8 @@ public int worldsCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
source.sendMessage(Text.of(TextColor.BLUE, "Worlds loaded by BlueMap:"));
for (var entry : plugin.getWorlds().entrySet()) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getValue().getName()).setHoverText(Text.of(entry.getValue().getSaveFolder(), TextColor.GRAY, " (" + entry.getKey() + ")")));
for (var entry : plugin.getBlueMap().getWorlds().entrySet()) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getKey()));
}
return 1;
@ -842,7 +842,7 @@ public int mapsCommand(CommandContext<S> context) {
List<Text> lines = new ArrayList<>();
lines.add(Text.of(TextColor.BLUE, "Maps loaded by BlueMap:"));
for (BmMap map : plugin.getMaps().values()) {
for (BmMap map : plugin.getBlueMap().getMaps().values()) {
boolean frozen = !plugin.getPluginState().getMapState(map).isUpdateEnabled();
lines.add(Text.of(TextColor.GRAY, " - ",
@ -850,7 +850,7 @@ public int mapsCommand(CommandContext<S> context) {
TextColor.GRAY, " (" + map.getName() + ")"));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0World: ",
TextColor.DARK_GRAY, map.getWorld().getName()));
TextColor.DARK_GRAY, map.getWorld().getId()));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Last Update: ",
TextColor.DARK_GRAY, helper.formatTime(map.getRenderState().getLatestRenderTime())));
@ -868,7 +868,7 @@ public int storagesCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource());
source.sendMessage(Text.of(TextColor.BLUE, "Storages loaded by BlueMap:"));
for (var entry : plugin.getBlueMap().getConfigs().getStorageConfigs().entrySet()) {
for (var entry : plugin.getBlueMap().getConfig().getStorageConfigs().entrySet()) {
source.sendMessage(Text.of(TextColor.GRAY, " - ", TextColor.WHITE, entry.getKey())
.setHoverText(Text.of(entry.getValue().getStorageType().name()))
.setClickAction(Text.ClickAction.RUN_COMMAND, "/bluemap storages " + entry.getKey())
@ -884,9 +884,10 @@ public int storagesInfoCommand(CommandContext<S> context) {
Storage storage;
try {
storage = plugin.getBlueMap().getStorage(storageId);
} catch (ConfigurationException ex) {
source.sendMessage(Text.of(TextColor.RED, ex.getMessage()));
storage = plugin.getBlueMap().getOrLoadStorage(storageId);
} catch (ConfigurationException | InterruptedException ex) {
Logger.global.logError("Unexpected exception trying to load storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to load this storage. Please check the console for more details..."));
return 0;
}
@ -894,8 +895,8 @@ public int storagesInfoCommand(CommandContext<S> context) {
try {
mapIds = storage.collectMapIds();
} catch (IOException ex) {
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to access this storage. Please check the console for more details..."));
Logger.global.logError("Unexpected exception trying to load mapIds from storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to access this storage. Please check the console for more details..."));
return 0;
}
@ -904,7 +905,7 @@ public int storagesInfoCommand(CommandContext<S> context) {
source.sendMessage(Text.of(TextColor.GRAY, " <empty storage>"));
} else {
for (String mapId : mapIds) {
BmMap map = plugin.getMaps().get(mapId);
BmMap map = plugin.getBlueMap().getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage);
if (isLoaded) {
@ -925,13 +926,14 @@ public int storagesDeleteMapCommand(CommandContext<S> context) {
Storage storage;
try {
storage = plugin.getBlueMap().getStorage(storageId);
} catch (ConfigurationException ex) {
source.sendMessage(Text.of(TextColor.RED, ex.getMessage()));
storage = plugin.getBlueMap().getOrLoadStorage(storageId);
} catch (ConfigurationException | InterruptedException ex) {
Logger.global.logError("Unexpected exception trying to load storage '" + storageId + "'!", ex);
source.sendMessage(Text.of(TextColor.RED, "There was an unexpected exception trying to load this storage. Please check the console for more details..."));
return 0;
}
BmMap map = plugin.getMaps().get(mapId);
BmMap map = plugin.getBlueMap().getMaps().get(mapId);
boolean isLoaded = map != null && map.getStorage().equals(storage);
if (isLoaded) {
Text purgeCommand = Text.of(TextColor.WHITE, "/bluemap purge " + mapId)

View File

@ -25,7 +25,6 @@
package de.bluecolored.bluemap.common.plugin.commands;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.map.BmMap;
import java.util.Collection;
import java.util.HashSet;
@ -40,7 +39,7 @@ public MapSuggestionProvider(Plugin plugin) {
@Override
public Collection<String> getPossibleValues() {
return new HashSet<>(plugin.getMaps().keySet());
return new HashSet<>(plugin.getBlueMap().getMaps().keySet());
}
}

View File

@ -38,7 +38,7 @@ public StorageSuggestionProvider(Plugin plugin) {
@Override
public Collection<String> getPossibleValues() {
return plugin.getBlueMap().getConfigs().getStorageConfigs().keySet();
return plugin.getBlueMap().getConfig().getStorageConfigs().keySet();
}
}

View File

@ -25,7 +25,6 @@
package de.bluecolored.bluemap.common.plugin.commands;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.world.World;
import java.util.Collection;
import java.util.HashSet;
@ -41,13 +40,8 @@ public WorldOrMapSuggestionProvider(Plugin plugin) {
@Override
public Collection<String> getPossibleValues() {
Collection<String> values = new HashSet<>();
for (World world : plugin.getWorlds().values()) {
values.add(world.getName());
}
values.addAll(plugin.getMaps().keySet());
values.addAll(plugin.getBlueMap().getWorlds().keySet());
values.addAll(plugin.getBlueMap().getMaps().keySet());
return values;
}

View File

@ -25,7 +25,6 @@
package de.bluecolored.bluemap.common.plugin.commands;
import de.bluecolored.bluemap.common.plugin.Plugin;
import de.bluecolored.bluemap.core.world.World;
import java.util.Collection;
import java.util.HashSet;
@ -40,13 +39,7 @@ public WorldSuggestionProvider(Plugin plugin) {
@Override
public Collection<String> getPossibleValues() {
Collection<String> values = new HashSet<>();
for (World world : plugin.getWorlds().values()) {
values.add(world.getName());
}
return values;
return new HashSet<>(plugin.getBlueMap().getWorlds().keySet());
}
}

View File

@ -83,7 +83,7 @@ public CompletableFuture<Void> updateSkin(final UUID playerUuid) {
return;
}
Map<String, BmMap> maps = plugin.getMaps();
Map<String, BmMap> maps = plugin.getBlueMap().getMaps();
if (maps == null) {
Logger.global.logDebug("Could not update skin, since the plugin seems not to be ready.");
return;

View File

@ -27,7 +27,7 @@
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import java.util.ArrayList;

View File

@ -27,11 +27,14 @@
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector2l;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import java.io.IOException;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -71,13 +74,17 @@ private synchronized void init() {
Set<Vector2l> tileSet = new HashSet<>();
startTime = System.currentTimeMillis();
//Logger.global.logInfo("Starting: " + worldRegion);
long changesSince = 0;
if (!force) changesSince = map.getRenderState().getRenderTime(worldRegion);
// collect chunks
long changesSince = force ? 0 : map.getRenderState().getRenderTime(worldRegion);
Region region = map.getWorld().getRegion(worldRegion.getX(), worldRegion.getY());
Collection<Vector2i> chunks = region.listChunks(changesSince);
Collection<Vector2i> chunks = new ArrayList<>(1024);
try {
region.iterateAllChunks((ChunkConsumer.ListOnly) (x, z, timestamp) -> {
if (timestamp >= changesSince) chunks.add(new Vector2i(x, z));
});
} catch (IOException ex) {
Logger.global.logWarning("Failed to read region " + worldRegion + " from world " + map.getWorld().getName() + " (" + ex + ")");
}
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
@ -115,6 +122,10 @@ private synchronized void init() {
.collect(Collectors.toCollection(ArrayDeque::new));
if (tiles.isEmpty()) complete();
else {
// preload chunks
map.getWorld().preloadRegionChunks(worldRegion.getX(), worldRegion.getY());
}
}
@Override
@ -132,7 +143,6 @@ public void doWork() {
this.atWork++;
}
//Logger.global.logInfo("Working on " + worldRegion + " - Tile " + tile);
if (tileRenderPreconditions(tile)) {
map.renderTile(tile); // <- actual work
}
@ -163,6 +173,7 @@ private boolean tileRenderPreconditions(Vector2i tile) {
for (int z = minChunk.getY(); z <= maxChunk.getY(); z++) {
Chunk chunk = map.getWorld().getChunk(x, z);
if (!chunk.isGenerated()) return false;
if (!chunk.hasLightData() && !map.getMapSettings().isIgnoreMissingLightData()) return false;
if (chunk.getInhabitedTime() >= minInhab) isInhabited = true;
}
}
@ -184,8 +195,6 @@ private boolean tileRenderPreconditions(Vector2i tile) {
private void complete() {
map.getRenderState().setRenderTime(worldRegion, startTime);
//Logger.global.logInfo("Done with: " + worldRegion);
}
@Override

View File

@ -35,7 +35,7 @@ public interface Player {
Text getName();
String getWorld();
ServerWorld getWorld();
Vector3d getPosition();
@ -48,8 +48,6 @@ public interface Player {
int getBlockLight();
boolean isOnline();
/**
* Return <code>true</code> if the player is sneaking.
* <p><i>If the player is offline the value of this method is undetermined.</i></p>

View File

@ -24,44 +24,21 @@
*/
package de.bluecolored.bluemap.common.serverinterface;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
public interface ServerInterface {
public interface Server {
@DebugDump
MinecraftVersion getMinecraftVersion();
/**
* Registers a ServerEventListener, every method of this interface should be called on the specified events
*/
void registerListener(ServerEventListener listener);
/**
* Removes all registered listeners
*/
void unregisterAllListeners();
default Optional<ServerWorld> getWorld(Path worldFolder) {
Path normalizedWorldFolder = worldFolder.toAbsolutePath().normalize();
return getLoadedWorlds().stream()
.filter(world -> world.getSaveFolder().toAbsolutePath().normalize().equals(normalizedWorldFolder))
.findAny();
}
default Optional<ServerWorld> getWorld(Object world) {
return Optional.empty();
}
@DebugDump
Collection<ServerWorld> getLoadedWorlds();
/**
* Returns the Folder containing the configurations for the plugin
*/
@ -82,6 +59,40 @@ default Tristate isMetricsEnabled() {
return Tristate.UNDEFINED;
}
/**
* Returns the correct {@link ServerWorld} for a {@link World} if there is any.
*/
default Optional<ServerWorld> getServerWorld(World world) {
if (world instanceof MCAWorld) {
MCAWorld mcaWorld = (MCAWorld) world;
return getLoadedServerWorlds().stream()
.filter(serverWorld ->
serverWorld.getWorldFolder().toAbsolutePath().normalize()
.equals(mcaWorld.getWorldFolder().toAbsolutePath().normalize()) &&
serverWorld.getDimension().equals(mcaWorld.getDimension())
)
.findAny();
}
return Optional.empty();
}
/**
* Returns the correct {@link ServerWorld} for any Object if there is any, this should return the correct ServerWorld
* for any implementation-specific object that represent or identify a world in any way.<br>
* Used for the API implementation.
*/
default Optional<ServerWorld> getServerWorld(Object world) {
if (world instanceof World)
return getServerWorld((World) world);
return Optional.empty();
}
/**
* Returns all loaded worlds of this server.
*/
@DebugDump
Collection<ServerWorld> getLoadedServerWorlds();
/**
* Returns a collection of the states of players that are currently online
*/
@ -89,9 +100,13 @@ default Tristate isMetricsEnabled() {
Collection<Player> getOnlinePlayers();
/**
* Returns the state of the player with that UUID if present<br>
* this method is only guaranteed to return a {@link Player} if the player is currently online.
* Registers a ServerEventListener, every method of this interface should be called on the specified events
*/
Optional<Player> getPlayer(UUID uuid);
void registerListener(ServerEventListener listener);
/**
* Removes all registered listeners
*/
void unregisterAllListeners();
}

View File

@ -25,40 +25,23 @@
package de.bluecolored.bluemap.common.serverinterface;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.util.Key;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
public interface ServerWorld {
@DebugDump
default Optional<String> getId() {
return Optional.empty();
}
Path getWorldFolder();
@DebugDump
default Optional<String> getName() {
return Optional.empty();
}
@DebugDump
Path getSaveFolder();
@DebugDump
default Dimension getDimension() {
Path saveFolder = getSaveFolder();
String lastName = saveFolder.getFileName().toString();
if (lastName.equals("DIM-1")) return Dimension.NETHER;
if (lastName.equals("DIM1")) return Dimension.END;
return Dimension.OVERWORLD;
}
Key getDimension();
/**
* Attempts to persist all changes that have been made in a world to disk.
*
* @return <code>true</code> if the changes have been successfully persisted, <code>false</code> if this operation is not supported by the implementation
*
* @throws IOException if something went wrong trying to persist the changes
*/
default boolean persistWorldChanges() throws IOException {

View File

@ -24,12 +24,14 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.BlueMap;
@DebugDump
public class BlueMapResponseModifier implements HttpRequestHandler {
private final HttpRequestHandler delegate;

View File

@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.*;
import org.apache.commons.lang3.time.DateFormatUtils;
@ -38,6 +39,7 @@
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
@DebugDump
public class FileRequestHandler implements HttpRequestHandler {
private final Path webRoot;

View File

@ -24,9 +24,11 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.core.logger.Logger;
@DebugDump
public class LoggingRequestHandler implements HttpRequestHandler {
private final HttpRequestHandler delegate;

View File

@ -27,7 +27,8 @@
import de.bluecolored.bluemap.common.config.PluginConfig;
import de.bluecolored.bluemap.common.live.LiveMarkersDataSupplier;
import de.bluecolored.bluemap.common.live.LivePlayersDataSupplier;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.Server;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.Storage;
import org.jetbrains.annotations.Nullable;
@ -38,9 +39,9 @@
public class MapRequestHandler extends RoutingRequestHandler {
public MapRequestHandler(BmMap map, ServerInterface serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
public MapRequestHandler(BmMap map, Server serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
this(map.getId(), map.getStorage(),
new LivePlayersDataSupplier(serverInterface, pluginConfig, map.getWorldId(), playerFilter),
createPlayersDataSupplier(map, serverInterface, pluginConfig, playerFilter),
new LiveMarkersDataSupplier(map.getMarkerSets()));
}
@ -67,4 +68,10 @@ public MapRequestHandler(String mapId, Storage mapStorage,
}
}
private static @Nullable LivePlayersDataSupplier createPlayersDataSupplier(BmMap map, Server serverInterface, PluginConfig pluginConfig, Predicate<UUID> playerFilter) {
ServerWorld world = serverInterface.getServerWorld(map.getWorld()).orElse(null);
if (world == null) return null;
return new LivePlayersDataSupplier(serverInterface, pluginConfig, world, playerFilter);
}
}

View File

@ -26,6 +26,7 @@
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.ContentTypeRegistry;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
@ -41,6 +42,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@DebugDump
public class MapStorageRequestHandler implements HttpRequestHandler {
private static final Pattern TILE_PATTERN = Pattern.compile("tiles/([\\d/]+)/x(-?[\\d/]+)z(-?[\\d/]+).*");

View File

@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
@ -34,6 +35,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@DebugDump
public class RoutingRequestHandler implements HttpRequestHandler {
public LinkedList<Route> routes;
@ -77,6 +79,7 @@ public HttpResponse handle(HttpRequest request) {
return new HttpResponse(HttpStatusCode.BAD_REQUEST);
}
@DebugDump
private static class Route {
private final Pattern routePattern;

View File

@ -24,8 +24,11 @@
*/
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.api.debug.DebugDump;
import java.io.IOException;
@DebugDump
public class HttpServer extends Server {
private final HttpRequestHandler requestHandler;

View File

@ -3,16 +3,20 @@
## Map-Config ##
## ##
# The name of this map
# This defines the display name of this map, you can change this at any time.
# Default is the id of this map
name: "${name}"
# The path to the save-folder of the world to render.
# (If this is not defined (commented out or removed), the map will be only registered to the web-server and the web-app
# but not rendered or loaded by BlueMap. This can be used to display a map that has been rendered somewhere else.)
world: "${world}"
# The dimension of the world. Can be "minecraft:overworld", "minecraft:the_nether", "minecraft:the_end"
# or any dimension-key introduced by a mod or datapack.
dimension: "${dimension}"
# The display-name of this map -> how this map will be named on the webapp.
# You can change this at any time.
# Default is the id of this map
name: "${name}"
# A lower value makes the map sorted first (in lists and menus), a higher value makes it sorted later.
# The value needs to be an integer but it can be negative.
# You can change this at any time.
@ -40,14 +44,6 @@ void-color: "${void-color}"
# Default is 0
ambient-light: ${ambient-light}
# Defines the skylight level that the sky of the world is emitting.
# This should always be equivalent to the maximum in-game sky-light for that world!
# If this is a normal overworld dimension, set this to 15 (max).
# If this is a normal nether or end dimension, set this to 0 (min).
# Changing this value requires a re-render of the map.
# Default is 15
world-sky-light: ${world-sky-light}
# BlueMap tries to omit all blocks that are below this Y-level and are not visible from above-ground.
# More specific: Block-Faces that have a sunlight/skylight value of 0 are removed.
# This improves the performance of the map on slower devices by a lot, but might cause some blocks to disappear that should normally be visible.

View File

@ -27,7 +27,12 @@ fun String.runCommand(): String = ProcessBuilder(split("\\s(?=(?:[^'\"`]*(['\"`]
}
val gitHash = "git rev-parse --verify HEAD".runCommand()
val clean = "git status --porcelain".runCommand().isEmpty()
var clean = false;
try {
clean = "git status --porcelain".runCommand().isEmpty();
} catch (ex: TimeoutException) {
println("Failed to run 'git status --porcelain', assuming dirty version.")
}
val lastTag = if ("git tag".runCommand().isEmpty()) "" else "git describe --tags --abbrev=0".runCommand()
val lastVersion = if (lastTag.isEmpty()) "dev" else lastTag.substring(1) // remove the leading 'v'
val commits = "git rev-list --count $lastTag..HEAD".runCommand()
@ -61,16 +66,22 @@ dependencies {
api ("commons-io:commons-io:2.5")
api ("org.spongepowered:configurate-hocon:4.1.2")
api ("org.spongepowered:configurate-gson:4.1.2")
api ("com.github.Querz:NBT:4.0")
api ("com.github.BlueMap-Minecraft:BlueNBT:v1.3.0")
api ("org.apache.commons:commons-dbcp2:2.9.0")
api ("io.airlift:aircompressor:0.24")
api ("org.lz4:lz4-java:1.8.0")
api ("de.bluecolored.bluemap.api:BlueMapAPI")
compileOnly ("org.jetbrains:annotations:23.0.0")
compileOnly ("org.projectlombok:lombok:1.18.28")
annotationProcessor ("org.projectlombok:lombok:1.18.28")
testImplementation ("org.junit.jupiter:junit-jupiter:5.8.2")
testRuntimeOnly ("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testCompileOnly ("org.projectlombok:lombok:1.18.28")
testAnnotationProcessor ("org.projectlombok:lombok:1.18.28")
}
spotless {

View File

@ -25,6 +25,8 @@
package de.bluecolored.bluemap.core.map;
import com.flowpowered.math.vector.Vector2i;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.api.gson.MarkerGson;
@ -35,7 +37,7 @@
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import java.io.*;
@ -55,9 +57,13 @@ public class BmMap {
public static final String META_FILE_MARKERS = "live/markers.json";
public static final String META_FILE_PLAYERS = "live/players.json";
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.registerTypeAdapter(BmMap.class, new MapSettingsSerializer())
.create();
private final String id;
private final String name;
private final String worldId;
private final World world;
private final Storage storage;
private final MapSettings mapSettings;
@ -76,10 +82,9 @@ public class BmMap {
private long renderTimeSumNanos;
private long tilesRendered;
public BmMap(String id, String name, String worldId, World world, Storage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
public BmMap(String id, String name, World world, Storage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.worldId = Objects.requireNonNull(worldId);
this.world = Objects.requireNonNull(world);
this.storage = Objects.requireNonNull(storage);
this.resourcePack = Objects.requireNonNull(resourcePack);
@ -197,10 +202,7 @@ private void saveMapSettings() {
OutputStream out = storage.writeMeta(id, META_FILE_SETTINGS);
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)
) {
ResourcesGson.addAdapter(new GsonBuilder())
.registerTypeAdapter(BmMap.class, new MapSettingsSerializer())
.create()
.toJson(this, writer);
GSON.toJson(this, writer);
} catch (Exception ex) {
Logger.global.logError("Failed to save settings for map '" + getId() + "'!", ex);
}
@ -235,10 +237,6 @@ public String getName() {
return name;
}
public String getWorldId() {
return worldId;
}
public World getWorld() {
return world;
}

View File

@ -26,14 +26,13 @@
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.map.hires.RenderSettings;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public interface MapSettings extends RenderSettings {
int getSorting();
Optional<Vector2i> getStartPos();
@Nullable Vector2i getStartPos();
String getSkyColor();

View File

@ -33,6 +33,7 @@
import de.bluecolored.bluemap.core.util.math.Color;
import java.lang.reflect.Type;
import java.util.Optional;
public class MapSettingsSerializer implements JsonSerializer<BmMap> {
@ -66,7 +67,7 @@ public JsonElement serialize(BmMap map, Type typeOfSrc, JsonSerializationContext
root.add("lowres", lowres);
// startPos
Vector2i startPos = map.getMapSettings().getStartPos()
Vector2i startPos = Optional.ofNullable(map.getMapSettings().getStartPos())
.orElse(map.getWorld().getSpawnPoint().toVector2(true));
root.add("startPos", context.serialize(startPos));

View File

@ -24,6 +24,9 @@
*/
package de.bluecolored.bluemap.core.map;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParseException;
import de.bluecolored.bluemap.api.debug.DebugDump;
@ -43,6 +46,10 @@
@DebugDump
public class TextureGallery {
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.create();
private final Map<ResourcePath<Texture>, TextureMapping> textureMappings;
private int nextId;
@ -93,7 +100,7 @@ public void writeTexturesFile(OutputStream out) throws IOException {
});
try (Writer writer = new OutputStreamWriter(out)) {
ResourcesGson.INSTANCE.toJson(textures, Texture[].class, writer);
GSON.toJson(textures, Texture[].class, writer);
} catch (JsonIOException ex) {
throw new IOException(ex);
}
@ -102,7 +109,7 @@ public void writeTexturesFile(OutputStream out) throws IOException {
public static TextureGallery readTexturesFile(InputStream in) throws IOException {
TextureGallery gallery = new TextureGallery();
try (Reader reader = new InputStreamReader(in)) {
Texture[] textures = ResourcesGson.INSTANCE.fromJson(reader, Texture[].class);
Texture[] textures = GSON.fromJson(reader, Texture[].class);
if (textures == null) throw new IOException("Texture data is empty!");
gallery.nextId = textures.length;
for (int ordinal = 0; ordinal < textures.length; ordinal++) {

View File

@ -31,7 +31,7 @@
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import java.io.IOException;

View File

@ -30,7 +30,8 @@
import de.bluecolored.bluemap.core.map.hires.blockmodel.BlockStateModelFactory;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.World;
public class HiresModelRenderer {
@ -68,13 +69,14 @@ public void render(World world, Vector3i modelMin, Vector3i modelMax, HiresTileM
for (z = min.getZ(); z <= max.getZ(); z++){
maxHeight = 0;
topBlockLight = 0f;
topBlockLight = 0;
columnColor.set(0, 0, 0, 0, true);
if (renderSettings.isInsideRenderBoundaries(x, z)) {
minY = Math.max(min.getY(), world.getMinY(x, z));
maxY = Math.min(max.getY(), world.getMaxY(x, z));
Chunk chunk = world.getChunkAtBlock(x, z);
minY = Math.max(min.getY(), chunk.getMinY(x, z));
maxY = Math.min(max.getY(), chunk.getMaxY(x, z));
for (y = minY; y <= maxY; y++) {
block.set(x, y, z);

View File

@ -65,11 +65,6 @@ default Vector3i getMaxPos() {
*/
float getAmbientLight();
/**
* The sky-light level of this world (0-15)
*/
int getWorldSkyLight();
/**
* The same as the maximum height, but blocks that are above this value are treated as AIR.<br>
* This leads to the top-faces being rendered instead of them being culled.
@ -78,6 +73,10 @@ default boolean isRenderEdges() {
return true;
}
default boolean isIgnoreMissingLightData() {
return false;
}
default boolean isInsideRenderBoundaries(int x, int z) {
Vector3i min = getMinPos();
Vector3i max = getMaxPos();

View File

@ -31,7 +31,7 @@
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.BlockState;
import java.util.ArrayList;

View File

@ -42,9 +42,9 @@
import de.bluecolored.bluemap.core.util.math.MatrixM3f;
import de.bluecolored.bluemap.core.util.math.VectorM2f;
import de.bluecolored.bluemap.core.util.math.VectorM3f;
import de.bluecolored.bluemap.core.world.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.ExtendedBlock;
import de.bluecolored.bluemap.core.world.block.ExtendedBlock;
/**
* A model builder for all liquid blocks
@ -71,7 +71,6 @@ public class LiquidModelBuilder {
private BlockModel modelResource;
private BlockModelView blockModel;
private Color blockColor;
private boolean isCave;
public LiquidModelBuilder(ResourcePack resourcePack, TextureGallery textureGallery, RenderSettings renderSettings) {
this.resourcePack = resourcePack;
@ -100,17 +99,19 @@ public void build(BlockNeighborhood<?> block, BlockState blockState, Variant var
this.blockModel = blockModel;
this.blockColor = color;
this.isCave =
this.block.getY() < renderSettings.getRemoveCavesBelowY() &&
this.block.getY() < block.getChunk().getOceanFloorY(block.getX(), block.getZ()) + renderSettings.getCaveDetectionOceanFloor();
build();
}
private final Color tintcolor = new Color();
private void build() {
int blockLight = block.getBlockLightLevel();
int sunLight = block.getSunLightLevel();
// filter out blocks that are in a "cave" that should not be rendered
if (this.isCave && (renderSettings.isCaveDetectionUsesBlockLight() ? block.getBlockLightLevel() : block.getSunLightLevel()) == 0f) return;
if (
this.block.isRemoveIfCave() &&
(renderSettings.isCaveDetectionUsesBlockLight() ? Math.max(blockLight, sunLight) : sunLight) == 0
) return;
int level = blockState.getLiquidLevel();
if (level < 8 && !(level == 0 && isSameLiquid(block.getNeighborBlock(0, 1, 0)))){
@ -165,7 +166,7 @@ private void build() {
blockColor.multiply(tintcolor);
// apply light
float combinedLight = Math.max(block.getSunLightLevel() / 15f, block.getBlockLightLevel() / 15f);
float combinedLight = Math.max(sunLight, blockLight) / 15f;
combinedLight = (renderSettings.getAmbientLight() + combinedLight) / (renderSettings.getAmbientLight() + 1f);
blockColor.r *= combinedLight;
blockColor.g *= combinedLight;

View File

@ -45,9 +45,9 @@
import de.bluecolored.bluemap.core.util.math.MatrixM4f;
import de.bluecolored.bluemap.core.util.math.VectorM2f;
import de.bluecolored.bluemap.core.util.math.VectorM3f;
import de.bluecolored.bluemap.core.world.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.BlockProperties;
import de.bluecolored.bluemap.core.world.ExtendedBlock;
import de.bluecolored.bluemap.core.world.block.ExtendedBlock;
import de.bluecolored.bluemap.core.world.LightData;
/**
@ -74,7 +74,6 @@ public class ResourceModelBuilder {
private BlockModelView blockModel;
private Color blockColor;
private float blockColorOpacity;
private boolean isCave;
public ResourceModelBuilder(ResourcePack resourcePack, TextureGallery textureGallery, RenderSettings renderSettings) {
this.resourcePack = resourcePack;
@ -95,10 +94,6 @@ public void build(BlockNeighborhood<?> block, Variant variant, BlockModelView bl
this.variant = variant;
this.modelResource = variant.getModel().getResource();
this.isCave =
this.block.getY() < renderSettings.getRemoveCavesBelowY() &&
this.block.getY() < block.getChunk().getOceanFloorY(block.getX(), block.getZ()) + renderSettings.getCaveDetectionOceanFloor();
this.tintColor.set(0, 0, 0, -1, true);
// render model
@ -201,7 +196,10 @@ private void createElementFace(Element element, Direction faceDir, VectorM3f c0,
int blockLight = Math.max(blockLightData.getBlockLight(), facedLightData.getBlockLight());
// filter out faces that are in a "cave" that should not be rendered
if (isCave && (renderSettings.isCaveDetectionUsesBlockLight() ? Math.max(blockLight, sunLight) : sunLight) == 0f) return;
if (
block.isRemoveIfCave() &&
(renderSettings.isCaveDetectionUsesBlockLight() ? Math.max(blockLight, sunLight) : sunLight) == 0
) return;
// initialize the faces
blockModel.initialize();

View File

@ -31,7 +31,7 @@
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.util.Grid;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jetbrains.annotations.Nullable;

View File

@ -27,7 +27,7 @@
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.util.Grid;
public class LowresTileManager implements TileMetaConsumer {

View File

@ -1,258 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@SuppressWarnings("FieldMayBeFinal")
public class ChunkAnvil113 extends MCAChunk {
private static final long[] EMPTY_LONG_ARRAY = new long[0];
private boolean isGenerated;
private boolean hasLight;
private long inhabitedTime;
private Section[] sections;
private int[] biomes;
private long[] oceanFloorHeights = EMPTY_LONG_ARRAY;
private long[] worldSurfaceHeights = EMPTY_LONG_ARRAY;
@SuppressWarnings("unchecked")
public ChunkAnvil113(MCAWorld world, CompoundTag chunkTag) {
super(world, chunkTag);
CompoundTag levelData = chunkTag.getCompoundTag("Level");
String status = levelData.getString("Status");
this.isGenerated = status.equals("full") ||
status.equals("fullchunk") ||
status.equals("postprocessed");
this.hasLight = isGenerated;
this.inhabitedTime = levelData.getLong("InhabitedTime");
if (!isGenerated && getWorld().isIgnoreMissingLightData()) {
isGenerated = !status.equals("empty");
}
if (levelData.containsKey("Heightmaps")) {
CompoundTag heightmapsTag = levelData.getCompoundTag("Heightmaps");
this.worldSurfaceHeights = heightmapsTag.getLongArray("WORLD_SURFACE");
this.oceanFloorHeights = heightmapsTag.getLongArray("OCEAN_FLOOR");
}
sections = new Section[32]; //32 supports a max world-height of 512 which is the max that the hightmaps of Minecraft V1.13+ can store with 9 bits, i believe?
if (levelData.containsKey("Sections")) {
for (CompoundTag sectionTag : ((ListTag<CompoundTag>) levelData.getListTag("Sections"))) {
Section section = new Section(sectionTag);
if (section.getSectionY() >= 0 && section.getSectionY() < sections.length) sections[section.getSectionY()] = section;
}
} else {
sections = new Section[0];
}
Tag<?> tag = levelData.get("Biomes"); //tag can be byte-array or int-array
if (tag instanceof ByteArrayTag) {
byte[] bs = ((ByteArrayTag) tag).getValue();
biomes = new int[bs.length];
for (int i = 0; i < bs.length; i++) {
biomes[i] = bs[i] & 0xFF;
}
}
else if (tag instanceof IntArrayTag) {
biomes = ((IntArrayTag) tag).getValue();
}
if (biomes == null || biomes.length == 0) {
biomes = new int[256];
}
if (biomes.length < 256) {
biomes = Arrays.copyOf(biomes, 256);
}
}
@Override
public boolean isGenerated() {
return isGenerated;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
int sectionY = y >> 4;
if (sectionY < 0 || sectionY >= this.sections.length) return BlockState.AIR;
Section section = this.sections[sectionY];
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLight) return target.set(getWorld().getSkyLight(), 0);
int sectionY = y >> 4;
if (sectionY < 0 || sectionY >= this.sections.length)
return (y < 0) ? target.set(0, 0) : target.set(getWorld().getSkyLight(), 0);
Section section = this.sections[sectionY];
if (section == null) return target.set(getWorld().getSkyLight(), 0);
return section.getLightData(x, y, z, target);
}
@Override
public String getBiome(int x, int y, int z) {
x &= 0xF; z &= 0xF;
int biomeIntIndex = z * 16 + x;
if (biomeIntIndex >= this.biomes.length) return Biome.DEFAULT.getFormatted();
return LegacyBiomes.idFor(biomes[biomeIntIndex]);
}
@Override
public int getWorldSurfaceY(int x, int z) {
if (this.worldSurfaceHeights.length < 36) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongStream(this.worldSurfaceHeights, z * 16 + x, 9);
}
@Override
public int getOceanFloorY(int x, int z) {
if (this.oceanFloorHeights.length < 36) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongStream(this.oceanFloorHeights, z * 16 + x, 9);
}
private static class Section {
private static final String AIR_ID = "minecraft:air";
private int sectionY;
private byte[] blockLight;
private byte[] skyLight;
private long[] blocks;
private BlockState[] palette;
private int bitsPerBlock;
@SuppressWarnings("unchecked")
public Section(CompoundTag sectionData) {
this.sectionY = sectionData.get("Y", NumberTag.class).asInt();
this.blockLight = sectionData.getByteArray("BlockLight");
this.skyLight = sectionData.getByteArray("SkyLight");
this.blocks = sectionData.getLongArray("BlockStates");
if (blocks.length < 256 && blocks.length > 0) blocks = Arrays.copyOf(blocks, 256);
if (blockLight.length < 2048 && blockLight.length > 0) blockLight = Arrays.copyOf(blockLight, 2048);
if (skyLight.length < 2048 && skyLight.length > 0) skyLight = Arrays.copyOf(skyLight, 2048);
//read block palette
ListTag<CompoundTag> paletteTag = (ListTag<CompoundTag>) sectionData.getListTag("Palette");
if (paletteTag != null) {
this.palette = new BlockState[paletteTag.size()];
for (int i = 0; i < this.palette.length; i++) {
CompoundTag stateTag = paletteTag.get(i);
String id = stateTag.getString("Name"); //shortcut to save time and memory
if (id.equals(AIR_ID)) {
palette[i] = BlockState.AIR;
continue;
}
Map<String, String> properties = new HashMap<>();
if (stateTag.containsKey("Properties")) {
CompoundTag propertiesTag = stateTag.getCompoundTag("Properties");
for (Entry<String, Tag<?>> property : propertiesTag) {
properties.put(property.getKey().toLowerCase(), ((StringTag) property.getValue()).getValue().toLowerCase());
}
}
palette[i] = new BlockState(id, properties);
}
} else {
this.palette = new BlockState[0];
}
this.bitsPerBlock = this.blocks.length >> 6; // available longs * 64 (bits per long) / 4096 (blocks per section) (floored result)
}
public int getSectionY() {
return sectionY;
}
public BlockState getBlockState(int x, int y, int z) {
if (palette.length == 1) return palette[0];
if (blocks.length == 0) return BlockState.AIR;
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockIndex = y * 256 + z * 16 + x;
long value = MCAMath.getValueFromLongStream(blocks, blockIndex, bitsPerBlock);
if (value >= palette.length) {
Logger.global.noFloodWarning("palettewarning", "Got palette value " + value + " but palette has size of " + palette.length + " (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return palette[(int) value];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockByteIndex = y * 256 + z * 16 + x;
int blockHalfByteIndex = blockByteIndex >> 1; // blockByteIndex / 2
boolean largeHalf = (blockByteIndex & 0x1) != 0; // (blockByteIndex % 2) == 0
return target.set(
this.skyLight.length > 0 ? MCAMath.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > 0 ? MCAMath.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
}
}

View File

@ -1,264 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@SuppressWarnings("FieldMayBeFinal")
public class ChunkAnvil115 extends MCAChunk {
private static final long[] EMPTY_LONG_ARRAY = new long[0];
private boolean isGenerated;
private boolean hasLight;
private long inhabitedTime;
private Section[] sections;
private int[] biomes;
private long[] oceanFloorHeights = EMPTY_LONG_ARRAY;
private long[] worldSurfaceHeights = EMPTY_LONG_ARRAY;
@SuppressWarnings("unchecked")
public ChunkAnvil115(MCAWorld world, CompoundTag chunkTag) {
super(world, chunkTag);
CompoundTag levelData = chunkTag.getCompoundTag("Level");
String status = levelData.getString("Status");
this.isGenerated = status.equals("full");
this.hasLight = isGenerated;
this.inhabitedTime = levelData.getLong("InhabitedTime");
if (!isGenerated && getWorld().isIgnoreMissingLightData()) {
isGenerated = !status.equals("empty");
}
if (levelData.containsKey("Heightmaps")) {
CompoundTag heightmapsTag = levelData.getCompoundTag("Heightmaps");
this.worldSurfaceHeights = heightmapsTag.getLongArray("WORLD_SURFACE");
this.oceanFloorHeights = heightmapsTag.getLongArray("OCEAN_FLOOR");
}
sections = new Section[32]; //32 supports a max world-height of 512 which is the max that the hightmaps of Minecraft V1.13+ can store with 9 bits, i believe?
if (levelData.containsKey("Sections")) {
for (CompoundTag sectionTag : ((ListTag<CompoundTag>) levelData.getListTag("Sections"))) {
Section section = new Section(sectionTag);
if (section.getSectionY() >= 0 && section.getSectionY() < sections.length) sections[section.getSectionY()] = section;
}
} else {
sections = new Section[0];
}
Tag<?> tag = levelData.get("Biomes"); //tag can be byte-array or int-array
if (tag instanceof ByteArrayTag) {
byte[] bs = ((ByteArrayTag) tag).getValue();
biomes = new int[bs.length];
for (int i = 0; i < bs.length; i++) {
biomes[i] = bs[i] & 0xFF;
}
}
else if (tag instanceof IntArrayTag) {
biomes = ((IntArrayTag) tag).getValue();
}
if (biomes == null || biomes.length == 0) {
biomes = new int[1024];
}
if (biomes.length < 1024) {
biomes = Arrays.copyOf(biomes, 1024);
}
}
@Override
public boolean isGenerated() {
return isGenerated;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
int sectionY = y >> 4;
if (sectionY < 0 || sectionY >= this.sections.length) return BlockState.AIR;
Section section = this.sections[sectionY];
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLight) return target.set(getWorld().getSkyLight(), 0);
int sectionY = y >> 4;
if (sectionY < 0 || sectionY >= this.sections.length)
return (y < 0) ? target.set(0, 0) : target.set(getWorld().getSkyLight(), 0);
Section section = this.sections[sectionY];
if (section == null) return target.set(getWorld().getSkyLight(), 0);
return section.getLightData(x, y, z, target);
}
@Override
public String getBiome(int x, int y, int z) {
x = (x & 0xF) / 4; // Math.floorMod(pos.getX(), 16)
z = (z & 0xF) / 4;
y = y / 4;
int biomeIntIndex = y * 16 + z * 4 + x;
if (biomeIntIndex < 0) return Biome.DEFAULT.getFormatted();
if (biomeIntIndex >= this.biomes.length) return Biome.DEFAULT.getFormatted();
return LegacyBiomes.idFor(biomes[biomeIntIndex]);
}
@Override
public int getMaxY(int x, int z) {
return sections.length * 16 + 15;
}
@Override
public int getWorldSurfaceY(int x, int z) {
if (this.worldSurfaceHeights.length < 36) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongStream(this.worldSurfaceHeights, z * 16 + x, 9);
}
@Override
public int getOceanFloorY(int x, int z) {
if (this.oceanFloorHeights.length < 36) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongStream(this.oceanFloorHeights, z * 16 + x, 9);
}
private static class Section {
private static final String AIR_ID = "minecraft:air";
private int sectionY;
private byte[] blockLight;
private byte[] skyLight;
private long[] blocks;
private BlockState[] palette;
private int bitsPerBlock;
@SuppressWarnings("unchecked")
public Section(CompoundTag sectionData) {
this.sectionY = sectionData.get("Y", NumberTag.class).asInt();
this.blockLight = sectionData.getByteArray("BlockLight");
this.skyLight = sectionData.getByteArray("SkyLight");
this.blocks = sectionData.getLongArray("BlockStates");
if (blocks.length < 256 && blocks.length > 0) blocks = Arrays.copyOf(blocks, 256);
if (blockLight.length < 2048 && blockLight.length > 0) blockLight = Arrays.copyOf(blockLight, 2048);
if (skyLight.length < 2048 && skyLight.length > 0) skyLight = Arrays.copyOf(skyLight, 2048);
//read block palette
ListTag<CompoundTag> paletteTag = (ListTag<CompoundTag>) sectionData.getListTag("Palette");
if (paletteTag != null) {
this.palette = new BlockState[paletteTag.size()];
for (int i = 0; i < this.palette.length; i++) {
CompoundTag stateTag = paletteTag.get(i);
String id = stateTag.getString("Name"); //shortcut to save time and memory
if (id.equals(AIR_ID)) {
palette[i] = BlockState.AIR;
continue;
}
Map<String, String> properties = new HashMap<>();
if (stateTag.containsKey("Properties")) {
CompoundTag propertiesTag = stateTag.getCompoundTag("Properties");
for (Entry<String, Tag<?>> property : propertiesTag) {
properties.put(property.getKey().toLowerCase(), ((StringTag) property.getValue()).getValue().toLowerCase());
}
}
palette[i] = new BlockState(id, properties);
}
} else {
this.palette = new BlockState[0];
}
this.bitsPerBlock = this.blocks.length >> 6; // available longs * 64 (bits per long) / 4096 (blocks per section) (floored result)
}
public int getSectionY() {
return sectionY;
}
public BlockState getBlockState(int x, int y, int z) {
if (palette.length == 1) return palette[0];
if (blocks.length == 0) return BlockState.AIR;
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockIndex = y * 256 + z * 16 + x;
long value = MCAMath.getValueFromLongStream(blocks, blockIndex, bitsPerBlock);
if (value >= palette.length) {
Logger.global.noFloodWarning("palettewarning", "Got palette value " + value + " but palette has size of " + palette.length + " (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return palette[(int) value];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockByteIndex = y * 256 + z * 16 + x;
int blockHalfByteIndex = blockByteIndex >> 1; // blockByteIndex / 2
boolean largeHalf = (blockByteIndex & 0x1) != 0; // (blockByteIndex % 2) == 0
return target.set(
this.skyLight.length > 0 ? MCAMath.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > 0 ? MCAMath.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
}
}

View File

@ -1,293 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
@SuppressWarnings("FieldMayBeFinal")
public class ChunkAnvil116 extends MCAChunk {
private static final long[] EMPTY_LONG_ARRAY = new long[0];
private boolean isGenerated;
private boolean hasLight;
private long inhabitedTime;
private int sectionMin, sectionMax;
private Section[] sections;
private int[] biomes;
private long[] oceanFloorHeights = EMPTY_LONG_ARRAY;
private long[] worldSurfaceHeights = EMPTY_LONG_ARRAY;
@SuppressWarnings("unchecked")
public ChunkAnvil116(MCAWorld world, CompoundTag chunkTag) {
super(world, chunkTag);
CompoundTag levelData = chunkTag.getCompoundTag("Level");
String status = levelData.getString("Status");
this.isGenerated = status.equals("full");
this.hasLight = isGenerated;
this.inhabitedTime = levelData.getLong("InhabitedTime");
if (!isGenerated && getWorld().isIgnoreMissingLightData()) {
isGenerated = !status.equals("empty");
}
if (levelData.containsKey("Heightmaps")) {
CompoundTag heightmapsTag = levelData.getCompoundTag("Heightmaps");
this.worldSurfaceHeights = heightmapsTag.getLongArray("WORLD_SURFACE");
this.oceanFloorHeights = heightmapsTag.getLongArray("OCEAN_FLOOR");
}
if (levelData.containsKey("Sections")) {
this.sectionMin = Integer.MAX_VALUE;
this.sectionMax = Integer.MIN_VALUE;
ListTag<CompoundTag> sectionsTag = (ListTag<CompoundTag>) levelData.getListTag("Sections");
ArrayList<Section> sectionList = new ArrayList<>(sectionsTag.size());
for (CompoundTag sectionTag : sectionsTag) {
if (sectionTag.getListTag("Palette") == null) continue; // ignore empty sections
Section section = new Section(sectionTag);
int y = section.getSectionY();
if (sectionMin > y) sectionMin = y;
if (sectionMax < y) sectionMax = y;
sectionList.add(section);
}
sections = new Section[1 + sectionMax - sectionMin];
for (Section section : sectionList) {
sections[section.sectionY - sectionMin] = section;
}
} else {
sections = new Section[0];
}
Tag<?> tag = levelData.get("Biomes"); //tag can be byte-array or int-array
if (tag instanceof ByteArrayTag) {
byte[] bs = ((ByteArrayTag) tag).getValue();
this.biomes = new int[bs.length];
for (int i = 0; i < bs.length; i++) {
biomes[i] = bs[i] & 0xFF;
}
}
else if (tag instanceof IntArrayTag) {
this.biomes = ((IntArrayTag) tag).getValue();
}
if (biomes == null) {
this.biomes = new int[0];
}
}
@Override
public boolean isGenerated() {
return isGenerated;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLight) return target.set(getWorld().getSkyLight(), 0);
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return (sectionY < sectionMin) ? target.set(0, 0) : target.set(getWorld().getSkyLight(), 0);
return section.getLightData(x, y, z, target);
}
@Override
public String getBiome(int x, int y, int z) {
if (biomes.length < 16) return Biome.DEFAULT.getFormatted();
x = (x & 0xF) / 4; // Math.floorMod(pos.getX(), 16)
z = (z & 0xF) / 4;
y = y / 4;
int biomeIntIndex = y * 16 + z * 4 + x; // TODO: fix this for 1.17+ worlds with negative y?
// shift y up/down if not in range
if (biomeIntIndex >= biomes.length) biomeIntIndex -= (((biomeIntIndex - biomes.length) >> 4) + 1) * 16;
if (biomeIntIndex < 0) biomeIntIndex -= (biomeIntIndex >> 4) * 16;
return LegacyBiomes.idFor(biomes[biomeIntIndex]);
}
@Override
public int getMinY(int x, int z) {
return sectionMin * 16;
}
@Override
public int getMaxY(int x, int z) {
return sectionMax * 16 + 15;
}
@Override
public int getWorldSurfaceY(int x, int z) {
if (this.worldSurfaceHeights.length < 37) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongArray(this.worldSurfaceHeights, z * 16 + x, 9);
}
@Override
public int getOceanFloorY(int x, int z) {
if (this.oceanFloorHeights.length < 37) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongArray(this.oceanFloorHeights, z * 16 + x, 9);
}
private Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
return this.sections[y];
}
private static class Section {
private static final String AIR_ID = "minecraft:air";
private int sectionY;
private byte[] blockLight;
private byte[] skyLight;
private long[] blocks;
private BlockState[] palette;
private int bitsPerBlock;
@SuppressWarnings("unchecked")
public Section(CompoundTag sectionData) {
this.sectionY = sectionData.get("Y", NumberTag.class).asInt();
this.blockLight = sectionData.getByteArray("BlockLight");
this.skyLight = sectionData.getByteArray("SkyLight");
this.blocks = sectionData.getLongArray("BlockStates");
if (blocks.length < 256 && blocks.length > 0) blocks = Arrays.copyOf(blocks, 256);
if (blockLight.length < 2048 && blockLight.length > 0) blockLight = Arrays.copyOf(blockLight, 2048);
if (skyLight.length < 2048 && skyLight.length > 0) skyLight = Arrays.copyOf(skyLight, 2048);
//read block palette
ListTag<CompoundTag> paletteTag = (ListTag<CompoundTag>) sectionData.getListTag("Palette");
if (paletteTag != null) {
this.palette = new BlockState[paletteTag.size()];
for (int i = 0; i < this.palette.length; i++) {
CompoundTag stateTag = paletteTag.get(i);
String id = stateTag.getString("Name"); //shortcut to save time and memory
if (id.equals(AIR_ID)) {
palette[i] = BlockState.AIR;
continue;
}
Map<String, String> properties = new HashMap<>();
if (stateTag.containsKey("Properties")) {
CompoundTag propertiesTag = stateTag.getCompoundTag("Properties");
for (Entry<String, Tag<?>> property : propertiesTag) {
properties.put(property.getKey().toLowerCase(), ((StringTag) property.getValue()).getValue().toLowerCase());
}
}
palette[i] = new BlockState(id, properties);
}
} else {
this.palette = new BlockState[0];
}
this.bitsPerBlock = this.blocks.length >> 6; // available longs * 64 (bits per long) / 4096 (blocks per section) (floored result)
}
public int getSectionY() {
return sectionY;
}
public BlockState getBlockState(int x, int y, int z) {
if (palette.length == 1) return palette[0];
if (blocks.length == 0) return BlockState.AIR;
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockIndex = y * 256 + z * 16 + x;
long value = MCAMath.getValueFromLongArray(blocks, blockIndex, bitsPerBlock);
if (value >= palette.length) {
Logger.global.noFloodWarning("palettewarning", "Got palette value " + value + " but palette has size of " + palette.length + "! (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return palette[(int) value];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockByteIndex = y * 256 + z * 16 + x;
int blockHalfByteIndex = blockByteIndex >> 1; // blockByteIndex / 2
boolean largeHalf = (blockByteIndex & 0x1) != 0; // (blockByteIndex % 2) == 0
return target.set(
this.skyLight.length > 0 ? MCAMath.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > 0 ? MCAMath.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
}
}

View File

@ -1,301 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.*;
import java.util.*;
import java.util.Map.Entry;
@SuppressWarnings("FieldMayBeFinal")
public class ChunkAnvil118 extends MCAChunk {
private static final long[] EMPTY_LONG_ARRAY = new long[0];
private static final BlockState[] EMPTY_BLOCK_STATE_ARRAY = new BlockState[0];
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private boolean isGenerated;
private boolean hasLight;
private long inhabitedTime;
private int sectionMin, sectionMax;
private Section[] sections;
private long[] oceanFloorHeights = EMPTY_LONG_ARRAY;
private long[] worldSurfaceHeights = EMPTY_LONG_ARRAY;
@SuppressWarnings("unchecked")
public ChunkAnvil118(MCAWorld world, CompoundTag chunkTag) {
super(world, chunkTag);
String status = chunkTag.getString("Status");
this.isGenerated = status.equals("full") || status.equals("minecraft:full");
this.hasLight = isGenerated;
this.inhabitedTime = chunkTag.getLong("InhabitedTime");
if (!isGenerated && getWorld().isIgnoreMissingLightData()) {
isGenerated = !status.equals("empty") && !status.equals("minecraft:empty");
}
if (chunkTag.containsKey("Heightmaps")) {
CompoundTag heightmapsTag = chunkTag.getCompoundTag("Heightmaps");
this.worldSurfaceHeights = heightmapsTag.getLongArray("WORLD_SURFACE");
this.oceanFloorHeights = heightmapsTag.getLongArray("OCEAN_FLOOR");
}
if (chunkTag.containsKey("sections")) {
this.sectionMin = Integer.MAX_VALUE;
this.sectionMax = Integer.MIN_VALUE;
ListTag<CompoundTag> sectionsTag = (ListTag<CompoundTag>) chunkTag.getListTag("sections");
ArrayList<Section> sectionList = new ArrayList<>(sectionsTag.size());
for (CompoundTag sectionTag : sectionsTag) {
Section section = new Section(sectionTag);
int y = section.getSectionY();
if (sectionMin > y) sectionMin = y;
if (sectionMax < y) sectionMax = y;
sectionList.add(section);
}
sections = new Section[1 + sectionMax - sectionMin];
for (Section section : sectionList) {
sections[section.sectionY - sectionMin] = section;
}
} else {
sections = new Section[0];
}
}
@Override
public boolean isGenerated() {
return isGenerated;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLight) return target.set(getWorld().getSkyLight(), 0);
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return (sectionY < sectionMin) ? target.set(0, 0) : target.set(getWorld().getSkyLight(), 0);
return section.getLightData(x, y, z, target);
}
@Override
public String getBiome(int x, int y, int z) {
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return Biome.DEFAULT.getFormatted();
return section.getBiome(x, y, z);
}
@Override
public int getMinY(int x, int z) {
return sectionMin * 16;
}
@Override
public int getMaxY(int x, int z) {
return sectionMax * 16 + 15;
}
@Override
public int getWorldSurfaceY(int x, int z) {
if (this.worldSurfaceHeights.length < 37) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongArray(this.worldSurfaceHeights, z * 16 + x, 9) - 64;
}
@Override
public int getOceanFloorY(int x, int z) {
if (this.oceanFloorHeights.length < 37) return 0;
x &= 0xF; z &= 0xF;
return (int) MCAMath.getValueFromLongArray(this.oceanFloorHeights, z * 16 + x, 9) - 64;
}
private Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
return this.sections[y];
}
private static class Section {
private int sectionY;
private byte[] blockLight;
private byte[] skyLight;
private long[] blocks = EMPTY_LONG_ARRAY;
private long[] biomes = EMPTY_LONG_ARRAY;
private BlockState[] blockPalette = EMPTY_BLOCK_STATE_ARRAY;
private String[] biomePalette = EMPTY_STRING_ARRAY;
private int bitsPerBlock, bitsPerBiome;
@SuppressWarnings("unchecked")
public Section(CompoundTag sectionData) {
this.sectionY = sectionData.get("Y", NumberTag.class).asInt();
this.blockLight = sectionData.getByteArray("BlockLight");
this.skyLight = sectionData.getByteArray("SkyLight");
// blocks
CompoundTag blockStatesTag = sectionData.getCompoundTag("block_states");
if (blockStatesTag != null) {
// block data
this.blocks = blockStatesTag.getLongArray("data");
// block palette
ListTag<CompoundTag> paletteTag = (ListTag<CompoundTag>) blockStatesTag.getListTag("palette");
if (paletteTag != null) {
this.blockPalette = new BlockState[paletteTag.size()];
for (int i = 0; i < this.blockPalette.length; i++) {
blockPalette[i] = readBlockStatePaletteEntry(paletteTag.get(i));
}
}
}
// biomes
CompoundTag biomesTag = sectionData.getCompoundTag("biomes");
if (biomesTag != null) {
// biomes data
this.biomes = biomesTag.getLongArray("data");
// biomes palette
ListTag<StringTag> paletteTag = (ListTag<StringTag>) biomesTag.getListTag("palette");
if (paletteTag != null) {
this.biomePalette = new String[paletteTag.size()];
for (int i = 0; i < this.biomePalette.length; i++) {
biomePalette[i] = paletteTag.get(i).getValue();
}
}
}
if (blocks.length < 256 && blocks.length > 0) blocks = Arrays.copyOf(blocks, 256);
if (blockLight.length < 2048 && blockLight.length > 0) blockLight = Arrays.copyOf(blockLight, 2048);
if (skyLight.length < 2048 && skyLight.length > 0) skyLight = Arrays.copyOf(skyLight, 2048);
this.bitsPerBlock = this.blocks.length >> 6; // available longs * 64 (bits per long) / 4096 (blocks per section) (floored result)
this.bitsPerBiome = Integer.SIZE - Integer.numberOfLeadingZeros(this.biomePalette.length - 1);
}
private BlockState readBlockStatePaletteEntry(CompoundTag paletteEntry) {
String id = paletteEntry.getString("Name");
if (BlockState.AIR.getFormatted().equals(id)) return BlockState.AIR; //shortcut to save time and memory
Map<String, String> properties = new LinkedHashMap<>();
if (paletteEntry.containsKey("Properties")) {
CompoundTag propertiesTag = paletteEntry.getCompoundTag("Properties");
for (Entry<String, Tag<?>> property : propertiesTag) {
properties.put(property.getKey().toLowerCase(), ((StringTag) property.getValue()).getValue().toLowerCase());
}
}
return new BlockState(id, properties);
}
public int getSectionY() {
return sectionY;
}
public BlockState getBlockState(int x, int y, int z) {
if (blockPalette.length == 1) return blockPalette[0];
if (blocks.length == 0) return BlockState.AIR;
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockIndex = y * 256 + z * 16 + x;
long value = MCAMath.getValueFromLongArray(blocks, blockIndex, bitsPerBlock);
if (value >= blockPalette.length) {
Logger.global.noFloodWarning("palettewarning", "Got block-palette value " + value + " but palette has size of " + blockPalette.length + "! (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return blockPalette[(int) value];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
x &= 0xF; y &= 0xF; z &= 0xF; // Math.floorMod(pos.getX(), 16)
int blockByteIndex = y * 256 + z * 16 + x;
int blockHalfByteIndex = blockByteIndex >> 1; // blockByteIndex / 2
boolean largeHalf = (blockByteIndex & 0x1) != 0; // (blockByteIndex % 2) == 0
return target.set(
this.skyLight.length > 0 ? MCAMath.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > 0 ? MCAMath.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
public String getBiome(int x, int y, int z) {
if (biomePalette.length == 0) return Biome.DEFAULT.getValue();
if (biomePalette.length == 1 || biomes.length == 0) return biomePalette[0];
x = (x & 0xF) / 4; // Math.floorMod(pos.getX(), 16) / 4
z = (z & 0xF) / 4;
y = (y & 0xF) / 4;
int biomeIndex = y * 16 + z * 4 + x;
long value = MCAMath.getValueFromLongArray(biomes, biomeIndex, bitsPerBiome);
if (value >= biomePalette.length) {
Logger.global.noFloodWarning("biomepalettewarning", "Got biome-palette value " + value + " but palette has size of " + biomePalette.length + "! (Future occasions of this error will not be logged)");
return Biome.DEFAULT.getValue();
}
return biomePalette[(int) value];
}
}
}

View File

@ -1,111 +0,0 @@
/*
* 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 de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.LightData;
import net.querz.nbt.CompoundTag;
import java.io.IOException;
public abstract class MCAChunk implements Chunk {
private final MCAWorld world;
private final int dataVersion;
protected MCAChunk() {
this.world = null;
this.dataVersion = -1;
}
protected MCAChunk(MCAWorld world) {
this.world = world;
this.dataVersion = -1;
}
protected MCAChunk(MCAWorld world, CompoundTag chunkTag) {
this.world = world;
dataVersion = chunkTag.getInt("DataVersion");
}
@Override
public abstract boolean isGenerated();
public int getDataVersion() {
return dataVersion;
}
@Override
public abstract long getInhabitedTime();
@Override
public abstract BlockState getBlockState(int x, int y, int z);
@Override
public abstract LightData getLightData(int x, int y, int z, LightData target);
@Override
public abstract String getBiome(int x, int y, int z);
@Override
public int getMaxY(int x, int z) {
return 255;
}
@Override
public int getMinY(int x, int z) {
return 0;
}
@Override
public int getWorldSurfaceY(int x, int z) { return 0; }
@Override
public int getOceanFloorY(int x, int z) { return 0; }
protected MCAWorld getWorld() {
return world;
}
public static MCAChunk create(MCAWorld world, CompoundTag chunkTag) throws IOException {
int version = chunkTag.getInt("DataVersion");
if (version < 2200) return new ChunkAnvil113(world, chunkTag);
if (version < 2500) return new ChunkAnvil115(world, chunkTag);
if (version < 2844) return new ChunkAnvil116(world, chunkTag);
return new ChunkAnvil118(world, chunkTag);
}
@Override
public String toString() {
return "MCAChunk{" +
"world=" + world +
"dataVersion=" + dataVersion +
"isGenerated()=" + isGenerated() +
'}';
}
}

View File

@ -1,289 +0,0 @@
/*
* 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 com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.mca.region.RegionType;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.world.*;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.NBTUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@DebugDump
public class MCAWorld implements World {
private static final Grid CHUNK_GRID = new Grid(16);
private static final Grid REGION_GRID = new Grid(32).multiply(CHUNK_GRID);
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
private final Path worldFolder;
private final String name;
private final Vector3i spawnPoint;
private final int skyLight;
private final boolean ignoreMissingLightData;
private final LoadingCache<Vector2i, Region> regionCache;
private final LoadingCache<Vector2i, Chunk> chunkCache;
public MCAWorld(Path worldFolder, int skyLight, boolean ignoreMissingLightData) throws IOException {
this.worldFolder = worldFolder.toRealPath();
this.skyLight = skyLight;
this.ignoreMissingLightData = ignoreMissingLightData;
this.regionCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(this::loadRegion);
this.chunkCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.maximumSize(500)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(this::loadChunk);
try {
Path levelFile = resolveLevelFile(worldFolder);
CompoundTag level = (CompoundTag) NBTUtil.readTag(levelFile.toFile());
CompoundTag levelData = level.getCompoundTag("Data");
this.name = levelData.getString("LevelName");
this.spawnPoint = new Vector3i(
levelData.getInt("SpawnX"),
levelData.getInt("SpawnY"),
levelData.getInt("SpawnZ")
);
} catch (ClassCastException | NullPointerException ex) {
throw new IOException("Invalid level.dat format!", ex);
}
}
@Override
public Chunk getChunkAtBlock(int x, int y, int z) {
return getChunk(x >> 4, z >> 4);
}
@Override
public Chunk getChunk(int x, int z) {
return getChunk(VECTOR_2_I_CACHE.get(x, z));
}
private Chunk getChunk(Vector2i pos) {
return chunkCache.get(pos);
}
@Override
public Region getRegion(int x, int z) {
return getRegion(VECTOR_2_I_CACHE.get(x, z));
}
private Region getRegion(Vector2i pos) {
return regionCache.get(pos);
}
@Override
public Collection<Vector2i> listRegions() {
File[] regionFiles = getRegionFolder().toFile().listFiles();
if (regionFiles == null) return Collections.emptyList();
List<Vector2i> regions = new ArrayList<>(regionFiles.length);
for (File file : regionFiles) {
if (RegionType.forFileName(file.getName()) == null) continue;
if (file.length() <= 0) continue;
try {
String[] filenameParts = file.getName().split("\\.");
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
regions.add(new Vector2i(rX, rZ));
} catch (NumberFormatException ignore) {}
}
return regions;
}
@Override
public String getName() {
return name;
}
@Override
public Path getSaveFolder() {
return worldFolder;
}
@Override
public int getSkyLight() {
return skyLight;
}
@Override
public int getMinY(int x, int z) {
return getChunk(x >> 4, z >> 4).getMinY(x, z);
}
@Override
public int getMaxY(int x, int z) {
return getChunk(x >> 4, z >> 4).getMaxY(x, z);
}
@Override
public Grid getChunkGrid() {
return CHUNK_GRID;
}
@Override
public Grid getRegionGrid() {
return REGION_GRID;
}
@Override
public Vector3i getSpawnPoint() {
return spawnPoint;
}
@Override
public void invalidateChunkCache() {
chunkCache.invalidateAll();
}
@Override
public void invalidateChunkCache(int x, int z) {
chunkCache.invalidate(VECTOR_2_I_CACHE.get(x, z));
}
@Override
public void cleanUpChunkCache() {
chunkCache.cleanUp();
}
public Path getWorldFolder() {
return worldFolder;
}
private Path getRegionFolder() {
return worldFolder.resolve("region");
}
public boolean isIgnoreMissingLightData() {
return ignoreMissingLightData;
}
private Region loadRegion(Vector2i regionPos) {
return loadRegion(regionPos.getX(), regionPos.getY());
}
Region loadRegion(int x, int z) {
return RegionType.loadRegion(this, getRegionFolder(), x, z);
}
private Chunk loadChunk(Vector2i chunkPos) {
return loadChunk(chunkPos.getX(), chunkPos.getY());
}
Chunk loadChunk(int x, int z) {
final int tries = 3;
final int tryInterval = 1000;
Exception loadException = null;
for (int i = 0; i < tries; i++) {
try {
return getRegion(x >> 5, z >> 5)
.loadChunk(x, z, ignoreMissingLightData);
} catch (IOException | RuntimeException e) {
if (loadException != null) e.addSuppressed(loadException);
loadException = e;
if (i + 1 < tries) {
try {
Thread.sleep(tryInterval);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
Logger.global.logDebug("Unexpected exception trying to load chunk (x:" + x + ", z:" + z + "):" + loadException);
return EmptyChunk.INSTANCE;
}
@Override
public String toString() {
return "MCAWorld{" +
"worldFolder=" + worldFolder +
", name='" + name + '\'' +
", spawnPoint=" + spawnPoint +
", skyLight=" + skyLight +
", ignoreMissingLightData=" + ignoreMissingLightData +
'}';
}
private static Path resolveLevelFile(Path worldFolder) throws IOException {
Path levelFolder = worldFolder.toRealPath();
Path levelFile = levelFolder.resolve("level.dat");
int searchDepth = 0;
while (!Files.isRegularFile(levelFile) && searchDepth < 4) {
searchDepth++;
levelFolder = levelFolder.getParent();
if (levelFolder == null) break;
levelFile = levelFolder.resolve("level.dat");
}
if (!Files.isRegularFile(levelFile))
throw new FileNotFoundException("Could not find a level.dat file for this world!");
return levelFile;
}
}

View File

@ -1,215 +0,0 @@
/*
* 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.region;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.mca.MCAChunk;
import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.EmptyChunk;
import de.bluecolored.bluemap.core.world.Region;
import io.airlift.compress.zstd.ZstdInputStream;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.Tag;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class LinearRegion implements Region {
public static final String FILE_SUFFIX = ".linear";
private static final List<Byte> SUPPORTED_VERSIONS = Arrays.asList((byte) 1, (byte) 2);
private static final long SUPERBLOCK = -4323716122432332390L;
private static final int HEADER_SIZE = 32;
private static final int FOOTER_SIZE = 8;
private final MCAWorld world;
private final Path regionFile;
private final Vector2i regionPos;
public LinearRegion(MCAWorld world, Path regionFile) throws IllegalArgumentException {
this.world = world;
this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\.");
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
this.regionPos = new Vector2i(rX, rZ);
}
@Override
public Chunk loadChunk(int chunkX, int chunkZ, boolean ignoreMissingLightData) throws IOException {
if (Files.notExists(regionFile)) return EmptyChunk.INSTANCE;
long fileLength = Files.size(regionFile);
if (fileLength == 0) return EmptyChunk.INSTANCE;
try (InputStream inputStream = Files.newInputStream(regionFile);
DataInputStream rawDataStream = new DataInputStream(inputStream)) {
long superBlock = rawDataStream.readLong();
if (superBlock != SUPERBLOCK)
throw new RuntimeException("Invalid superblock: " + superBlock + " file " + regionFile);
byte version = rawDataStream.readByte();
if (!SUPPORTED_VERSIONS.contains(version))
throw new RuntimeException("Invalid version: " + version + " file " + regionFile);
// Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused.
rawDataStream.skipBytes(11);
int dataCount = rawDataStream.readInt();
if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE)
throw new RuntimeException("Invalid file length: " + this.regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE));
// Skip data hash (Long): Unused.
rawDataStream.skipBytes(8);
byte[] rawCompressed = new byte[dataCount];
rawDataStream.readFully(rawCompressed, 0, dataCount);
superBlock = rawDataStream.readLong();
if (superBlock != SUPERBLOCK)
throw new RuntimeException("Invalid footer superblock: " + this.regionFile);
try (DataInputStream dis = new DataInputStream(new ZstdInputStream(new ByteArrayInputStream(rawCompressed)))) {
int x = chunkX - (regionPos.getX() << 5);
int z = chunkZ - (regionPos.getY() << 5);
int pos = (z << 5) + x;
int skip = 0;
for (int i = 0; i < pos; i++) {
skip += dis.readInt(); // Size of the chunk (bytes) to skip
dis.skipBytes(4); // Skip timestamps
}
int size = dis.readInt();
if (size <= 0) return EmptyChunk.INSTANCE;
dis.skipBytes(((1024 - pos - 1) << 3) + 4); // Skip current chunk 0 and unneeded other chunks zero/size
dis.skipBytes(skip); // Skip unneeded chunks data
Tag<?> tag = Tag.deserialize(dis, Tag.DEFAULT_MAX_DEPTH);
if (tag instanceof CompoundTag) {
MCAChunk chunk = MCAChunk.create(world, (CompoundTag) tag);
if (!chunk.isGenerated()) return EmptyChunk.INSTANCE;
return chunk;
} else {
throw new IOException("Invalid data tag: " + (tag == null ? "null" : tag.getClass().getName()));
}
}
} catch (RuntimeException e) {
throw new IOException(e);
}
}
@Override
public Collection<Vector2i> listChunks(long modifiedSince) {
if (Files.notExists(regionFile)) return Collections.emptyList();
long fileLength;
try {
fileLength = Files.size(regionFile);
if (fileLength == 0) return Collections.emptyList();
} catch (IOException ex) {
Logger.global.logWarning("Failed to read file-size for file: " + regionFile);
return Collections.emptyList();
}
List<Vector2i> chunks = new ArrayList<>(1024); //1024 = 32 x 32 chunks per region-file
try (InputStream inputStream = Files.newInputStream(regionFile);
DataInputStream rawDataStream = new DataInputStream(inputStream)) {
long superBlock = rawDataStream.readLong();
if (superBlock != SUPERBLOCK)
throw new RuntimeException("Invalid superblock: " + superBlock + " file " + regionFile);
byte version = rawDataStream.readByte();
if (!SUPPORTED_VERSIONS.contains(version))
throw new RuntimeException("Invalid version: " + version + " file " + regionFile);
int date = (int) (modifiedSince / 1000);
// If whole region is the same - skip.
long newestTimestamp = rawDataStream.readLong();
if (newestTimestamp < date) return Collections.emptyList();
// Linear v1 files store whole region timestamp, not chunk timestamp. We need to render the whole region file.
if (version == 1) {
for(int i = 0 ; i < 1024; i++)
chunks.add(new Vector2i((regionPos.getX() << 5) + (i & 31), (regionPos.getY() << 5) + (i >> 5)));
return chunks;
}
// Linear v2: Chunk timestamps are here!
// Skip Compression level (Byte) + Chunk count (Short): Unused.
rawDataStream.skipBytes(3);
int dataCount = rawDataStream.readInt();
if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE)
throw new RuntimeException("Invalid file length: " + this.regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE));
// Skip data hash (Long): Unused.
rawDataStream.skipBytes(8);
byte[] rawCompressed = new byte[dataCount];
rawDataStream.readFully(rawCompressed, 0, dataCount);
superBlock = rawDataStream.readLong();
if (superBlock != SUPERBLOCK)
throw new RuntimeException("Invalid footer SuperBlock: " + this.regionFile);
try (DataInputStream dis = new DataInputStream(new ZstdInputStream(new ByteArrayInputStream(rawCompressed)))) {
for (int i = 0; i < 1024; i++) {
dis.skipBytes(4); // Skip size of the chunk
int timestamp = dis.readInt();
if (timestamp >= date) // Timestamps
chunks.add(new Vector2i((regionPos.getX() << 5) + (i & 31), (regionPos.getY() << 5) + (i >> 5)));
}
}
} catch (RuntimeException | IOException ex) {
Logger.global.logWarning("Failed to read .linear file: " + regionFile + " (" + ex + ")");
}
return chunks;
}
@Override
public Path getRegionFile() {
return regionFile;
}
public static String getRegionFileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX;
}
}

View File

@ -1,164 +0,0 @@
/*
* 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.region;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.mca.MCAChunk;
import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.EmptyChunk;
import de.bluecolored.bluemap.core.world.Region;
import net.querz.nbt.CompoundTag;
import net.querz.nbt.Tag;
import net.querz.nbt.mca.CompressionType;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MCARegion implements Region {
public static final String FILE_SUFFIX = ".mca";
private final MCAWorld world;
private final Path regionFile;
private final Vector2i regionPos;
public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentException {
this.world = world;
this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\.");
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
this.regionPos = new Vector2i(rX, rZ);
}
@Override
public Chunk loadChunk(int chunkX, int chunkZ, boolean ignoreMissingLightData) throws IOException {
if (Files.notExists(regionFile)) return EmptyChunk.INSTANCE;
long fileLength = Files.size(regionFile);
if (fileLength == 0) return EmptyChunk.INSTANCE;
try (RandomAccessFile raf = new RandomAccessFile(regionFile.toFile(), "r")) {
int xzChunk = Math.floorMod(chunkZ, 32) * 32 + Math.floorMod(chunkX, 32);
raf.seek(xzChunk * 4L);
int offset = raf.read() << 16;
offset |= (raf.read() & 0xFF) << 8;
offset |= raf.read() & 0xFF;
offset *= 4096;
int size = raf.readByte() * 4096;
if (size == 0) {
return EmptyChunk.INSTANCE;
}
raf.seek(offset + 4); // +4 skip chunk size
byte compressionTypeByte = raf.readByte();
CompressionType compressionType = compressionTypeByte == 3 ?
CompressionType.NONE :
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) {
MCAChunk chunk = MCAChunk.create(world, (CompoundTag) tag);
if (!chunk.isGenerated()) return EmptyChunk.INSTANCE;
return chunk;
} else {
throw new IOException("Invalid data tag: " + (tag == null ? "null" : tag.getClass().getName()));
}
} catch (RuntimeException e) {
throw new IOException(e);
}
}
@Override
public Collection<Vector2i> listChunks(long modifiedSince) {
if (Files.notExists(regionFile)) return Collections.emptyList();
try {
long fileLength = Files.size(regionFile);
if (fileLength == 0) return Collections.emptyList();
} catch (IOException ex) {
Logger.global.logWarning("Failed to read file-size for file: " + regionFile);
return Collections.emptyList();
}
List<Vector2i> chunks = new ArrayList<>(1024); //1024 = 32 x 32 chunks per region-file
try (RandomAccessFile raf = new RandomAccessFile(regionFile.toFile(), "r")) {
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
Vector2i chunk = new Vector2i(regionPos.getX() * 32 + x, regionPos.getY() * 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 >= (modifiedSince / 1000)) {
chunks.add(chunk);
}
}
}
} catch (RuntimeException | IOException ex) {
Logger.global.logWarning("Failed to read .mca file: " + regionFile + " (" + ex + ")");
}
return chunks;
}
@Override
public Path getRegionFile() {
return regionFile;
}
public static String getRegionFileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX;
}
}

View File

@ -29,7 +29,7 @@
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
@ -42,6 +42,14 @@
@DebugDump
public class BlockColorCalculatorFactory {
private static final int
AVERAGE_MIN_X = - 2,
AVERAGE_MAX_X = 2,
AVERAGE_MIN_Y = - 1,
AVERAGE_MAX_Y = 1,
AVERAGE_MIN_Z = - 2,
AVERAGE_MAX_Z = 2;
private final int[] foliageMap = new int[65536];
private final int[] grassMap = new int[65536];
@ -133,18 +141,12 @@ public Color getRedstoneColor(BlockNeighborhood<?> block, Color target) {
public Color getWaterAverageColor(BlockNeighborhood<?> block, Color target) {
target.set(0, 0, 0, 0, true);
int x, y, z,
minX = - 2,
maxX = 2,
minY = - 1,
maxY = 1,
minZ = - 2,
maxZ = 2;
int x, y, z;
Biome biome;
for (x = minX; x <= maxX; x++) {
for (y = minY; y <= maxY; y++) {
for (z = minZ; z <= maxZ; z++) {
for (y = AVERAGE_MIN_Y; y <= AVERAGE_MAX_Y; y++) {
for (x = AVERAGE_MIN_X; x <= AVERAGE_MAX_X; x++) {
for (z = AVERAGE_MIN_Z; z <= AVERAGE_MAX_Z; z++) {
biome = block.getNeighborBlock(x, y, z).getBiome();
target.add(biome.getWaterColor());
}
@ -157,18 +159,12 @@ public Color getWaterAverageColor(BlockNeighborhood<?> block, Color target) {
public Color getFoliageAverageColor(BlockNeighborhood<?> block, Color target) {
target.set(0, 0, 0, 0, true);
int x, y, z,
minX = - 2,
maxX = 2,
minY = - 1,
maxY = 1,
minZ = - 2,
maxZ = 2;
int x, y, z;
Biome biome;
for (y = minY; y <= maxY; y++) {
for (x = minX; x <= maxX; x++) {
for (z = minZ; z <= maxZ; z++) {
for (y = AVERAGE_MIN_Y; y <= AVERAGE_MAX_Y; y++) {
for (x = AVERAGE_MIN_X; x <= AVERAGE_MAX_X; x++) {
for (z = AVERAGE_MIN_Z; z <= AVERAGE_MAX_Z; z++) {
biome = block.getNeighborBlock(x, y, z).getBiome();
target.add(getFoliageColor(biome, tempColor));
}
@ -186,18 +182,12 @@ public Color getFoliageColor(Biome biome, Color target) {
public Color getGrassAverageColor(BlockNeighborhood<?> block, Color target) {
target.set(0, 0, 0, 0, true);
int x, y, z,
minX = - 2,
maxX = 2,
minY = - 1,
maxY = 1,
minZ = - 2,
maxZ = 2;
int x, y, z;
Biome biome;
for (y = minY; y <= maxY; y++) {
for (x = minX; x <= maxX; x++) {
for (z = minZ; z <= maxZ; z++) {
for (y = AVERAGE_MIN_Y; y <= AVERAGE_MAX_Y; y++) {
for (x = AVERAGE_MIN_X; x <= AVERAGE_MAX_X; x++) {
for (z = AVERAGE_MIN_Z; z <= AVERAGE_MAX_Z; z++) {
biome = block.getNeighborBlock(x, y, z).getBiome();
target.add(getGrassColor(biome, tempColor));
}

View File

@ -77,9 +77,6 @@ private static String parsePath(Path filePath) {
if (filePath.getNameCount() < 4)
throw new IllegalArgumentException("The provided filePath has less than 4 segments!");
if (!filePath.getName(0).toString().equalsIgnoreCase("assets"))
throw new IllegalArgumentException("The provided filePath doesn't start with 'assets'!");
String namespace = filePath.getName(1).toString();
String path = filePath.subpath(3, filePath.getNameCount()).toString().replace(filePath.getFileSystem().getSeparator(), "/");

View File

@ -25,22 +25,21 @@
package de.bluecolored.bluemap.core.resources.adapter;
import com.flowpowered.math.vector.*;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.Face;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.util.math.Axis;
import de.bluecolored.bluemap.core.util.math.Color;
import java.io.IOException;
import java.util.EnumMap;
public class ResourcesGson {
public static final Gson INSTANCE = addAdapter(new GsonBuilder())
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.setLenient()
.create();
@ -60,9 +59,4 @@ public static GsonBuilder addAdapter(GsonBuilder builder) {
);
}
public static String nextStringOrBoolean(JsonReader in) throws IOException {
if (in.peek() == JsonToken.BOOLEAN) return Boolean.toString(in.nextBoolean());
return in.nextString();
}
}

View File

@ -25,8 +25,10 @@
package de.bluecolored.bluemap.core.resources.biome.datapack;
import de.bluecolored.bluemap.core.world.Biome;
import lombok.Getter;
@SuppressWarnings("FieldMayBeFinal")
@Getter
public class DpBiome {
private DpBiomeEffects effects = new DpBiomeEffects();
@ -44,16 +46,4 @@ public Biome createBiome(String formatted) {
);
}
public DpBiomeEffects getEffects() {
return effects;
}
public double getTemperature() {
return temperature;
}
public double getDownfall() {
return downfall;
}
}

View File

@ -26,24 +26,14 @@
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Biome;
import lombok.Getter;
@SuppressWarnings("FieldMayBeFinal")
@Getter
public class DpBiomeEffects {
private Color water_color = Biome.DEFAULT.getWaterColor();
private Color foliage_color = Biome.DEFAULT.getOverlayFoliageColor();
private Color grass_color = Biome.DEFAULT.getOverlayGrassColor();
public Color getWaterColor() {
return water_color;
}
public Color getFoliageColor() {
return foliage_color;
}
public Color getGrassColor() {
return grass_color;
}
private Color waterColor = Biome.DEFAULT.getWaterColor();
private Color foliageColor = Biome.DEFAULT.getOverlayFoliageColor();
private Color grassColor = Biome.DEFAULT.getOverlayGrassColor();
}

View File

@ -0,0 +1,117 @@
package de.bluecolored.bluemap.core.resources.datapack;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.datapack.dimension.DimensionTypeData;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.DimensionType;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
public class DataPack {
public static final Key DIMENSION_OVERWORLD = new Key("minecraft", "overworld");
public static final Key DIMENSION_THE_NETHER = new Key("minecraft", "the_nether");
public static final Key DIMENSION_THE_END = new Key("minecraft", "the_end");
public static final Key DIMENSION_TYPE_OVERWORLD = new Key("minecraft", "overworld");
public static final Key DIMENSION_TYPE_OVERWORLD_CAVES = new Key("minecraft", "overworld_caves");
public static final Key DIMENSION_TYPE_THE_NETHER = new Key("minecraft", "the_nether");
public static final Key DIMENSION_TYPE_THE_END = new Key("minecraft", "the_end");
private final Map<Key, DimensionType> dimensionTypes = new HashMap<>();
@Nullable
public DimensionType getDimensionType(Key key) {
return dimensionTypes.get(key);
}
public void load(Path root) throws InterruptedException {
Logger.global.logDebug("Loading datapack from: " + root + " ...");
loadPath(root);
}
private void loadPath(Path root) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
if (!Files.isDirectory(root)) {
try (FileSystem fileSystem = FileSystems.newFileSystem(root, (ClassLoader) null)) {
for (Path fsRoot : fileSystem.getRootDirectories()) {
if (!Files.isDirectory(fsRoot)) continue;
loadPath(fsRoot);
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read '" + root + "': " + ex);
}
return;
}
list(root.resolve("data"))
.map(path -> path.resolve("dimension_type"))
.filter(Files::isDirectory)
.flatMap(DataPack::walk)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.filter(Files::isRegularFile)
.forEach(file -> loadResource(root, file, () -> {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return ResourcesGson.INSTANCE.fromJson(reader, DimensionTypeData.class);
}
}, dimensionTypes));
}
private <T> void loadResource(Path root, Path file, Loader<T> loader, Map<Key, T> resultMap) {
try {
ResourcePath<T> resourcePath = new ResourcePath<>(root.relativize(file));
if (resultMap.containsKey(resourcePath)) return; // don't load already present resources
T resource = loader.load();
if (resource == null) return; // don't load missing resources
resourcePath.setResource(resource);
resultMap.put(resourcePath, resource);
} catch (Exception ex) {
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
}
}
public void bake() {
dimensionTypes.putIfAbsent(DIMENSION_TYPE_OVERWORLD, DimensionType.OVERWORLD);
dimensionTypes.putIfAbsent(DIMENSION_TYPE_OVERWORLD_CAVES, DimensionType.OVERWORLD_CAVES);
dimensionTypes.putIfAbsent(DIMENSION_TYPE_THE_NETHER, DimensionType.NETHER);
dimensionTypes.putIfAbsent(DIMENSION_TYPE_THE_END, DimensionType.END);
}
private static Stream<Path> list(Path root) {
if (!Files.isDirectory(root)) return Stream.empty();
try {
return Files.list(root);
} catch (IOException ex) {
throw new CompletionException(ex);
}
}
private static Stream<Path> walk(Path root) {
if (!Files.exists(root)) return Stream.empty();
if (Files.isRegularFile(root)) return Stream.of(root);
try {
return Files.walk(root);
} catch (IOException ex) {
throw new CompletionException(ex);
}
}
private interface Loader<T> {
T load() throws IOException;
}
}

View File

@ -0,0 +1,21 @@
package de.bluecolored.bluemap.core.resources.datapack.dimension;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.world.DimensionType;
import lombok.*;
import lombok.experimental.Accessors;
@Data
@DebugDump
public class DimensionTypeData implements DimensionType {
private boolean natural;
@Accessors(fluent = true) private boolean hasSkylight;
@Accessors(fluent = true) private boolean hasCeiling;
private float ambientLight;
private int minY;
private int height;
private Long fixedTime;
private double coordinateScale;
}

View File

@ -183,20 +183,26 @@ private BlockProperties loadBlockProperties(de.bluecolored.bluemap.core.world.Bl
return props.build();
}
public synchronized void loadResources(Iterable<Path> roots) throws IOException {
public synchronized void loadResources(Iterable<Path> roots) throws IOException, InterruptedException {
Logger.global.logInfo("Loading resources...");
for (Path root : roots) {
if (Thread.interrupted()) throw new InterruptedException();
Logger.global.logDebug("Loading resources from: " + root + " ...");
loadResourcePath(root, this::loadResources);
}
Logger.global.logInfo("Loading textures...");
for (Path root : roots) {
if (Thread.interrupted()) throw new InterruptedException();
Logger.global.logDebug("Loading textures from: " + root + " ...");
loadResourcePath(root, this::loadTextures);
}
if (Thread.interrupted()) throw new InterruptedException();
Logger.global.logInfo("Baking resources...");
bake();
@ -204,7 +210,8 @@ public synchronized void loadResources(Iterable<Path> roots) throws IOException
Logger.global.logInfo("Resources loaded.");
}
private void loadResourcePath(Path root, PathLoader resourceLoader) throws IOException {
private void loadResourcePath(Path root, PathLoader resourceLoader) throws IOException, InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
if (!Files.isDirectory(root)) {
try (FileSystem fileSystem = FileSystems.newFileSystem(root, (ClassLoader) null)) {
for (Path fsRoot : fileSystem.getRootDirectories()) {
@ -299,6 +306,7 @@ private void loadResources(Path root) throws IOException {
}, BlueMap.THREAD_POOL),
// load biome configs
// TODO: move this to datapacks?
CompletableFuture.runAsync(() -> {
list(root.resolve("assets"))
.map(path -> path.resolve("biomes.json"))
@ -386,7 +394,7 @@ private void loadTextures(Path root) throws IOException {
}
}
private void bake() throws IOException {
private void bake() throws IOException, InterruptedException {
// fill path maps
blockStates.keySet().forEach(path -> blockStatePaths.put(path.getFormatted(), path));
@ -398,11 +406,15 @@ private void bake() throws IOException {
model.optimize(this);
}
if (Thread.interrupted()) throw new InterruptedException();
// apply model parents
for (BlockModel model : blockModels.values()) {
model.applyParent(this);
}
if (Thread.interrupted()) throw new InterruptedException();
// calculate model properties
for (BlockModel model : blockModels.values()) {
model.calculateProperties(this);

View File

@ -27,9 +27,9 @@
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.world.BlockState;
import org.apache.commons.lang3.StringUtils;
@ -125,7 +125,7 @@ public BlockStateCondition readCondition(JsonReader in) throws IOException {
andConditions.add(
BlockStateCondition.and(andArray.toArray(new BlockStateCondition[0])));
} else {
String[] values = StringUtils.split(ResourcesGson.nextStringOrBoolean(in), '|');
String[] values = StringUtils.split(nextStringOrBoolean(in), '|');
andConditions.add(BlockStateCondition.property(name, values));
}
}
@ -134,6 +134,11 @@ public BlockStateCondition readCondition(JsonReader in) throws IOException {
return BlockStateCondition.and(andConditions.toArray(new BlockStateCondition[0]));
}
private String nextStringOrBoolean(JsonReader in) throws IOException {
if (in.peek() == JsonToken.BOOLEAN) return Boolean.toString(in.nextBoolean());
return in.nextString();
}
}
}

View File

@ -24,16 +24,16 @@
*/
package de.bluecolored.bluemap.core.storage;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class CompressedInputStream extends InputStream {
public class CompressedInputStream extends FilterInputStream {
private final InputStream in;
private final Compression compression;
public CompressedInputStream(InputStream in, Compression compression) {
this.in = in;
super(in);
this.compression = compression;
}
@ -45,29 +45,4 @@ public Compression getCompression() {
return compression;
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void reset() throws IOException {
in.reset();
}
}

View File

@ -26,22 +26,25 @@
import io.airlift.compress.zstd.ZstdInputStream;
import io.airlift.compress.zstd.ZstdOutputStream;
import net.jpountz.lz4.LZ4FrameInputStream;
import net.jpountz.lz4.LZ4FrameOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.NoSuchElementException;
import java.util.zip.DeflaterInputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.InflaterInputStream;
public enum Compression {
NONE("none", "", out -> out, in -> in),
GZIP("gzip", ".gz", GZIPOutputStream::new, GZIPInputStream::new),
DEFLATE("deflate", ".deflate", DeflaterOutputStream::new, DeflaterInputStream::new),
ZSTD("zstd", ".zst", ZstdOutputStream::new, ZstdInputStream::new);
DEFLATE("deflate", ".deflate", DeflaterOutputStream::new, InflaterInputStream::new),
ZSTD("zstd", ".zst", ZstdOutputStream::new, ZstdInputStream::new),
LZ4("lz4", ".lz4", LZ4FrameOutputStream::new, LZ4FrameInputStream::new);
private final String typeId;
private final String fileSuffix;

View File

@ -29,7 +29,7 @@
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.sql.dialect.Dialect;
import de.bluecolored.bluemap.core.storage.sql.dialect.PostgresDialect;
import de.bluecolored.bluemap.core.util.WrappedOutputStream;
import de.bluecolored.bluemap.core.util.OnCloseOutputStream;
import java.io.*;
import java.net.MalformedURLException;
@ -51,7 +51,7 @@ public PostgreSQLStorage(Dialect dialect, SQLStorageSettings config) throws Malf
public OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IOException {
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new WrappedOutputStream(compression.compress(byteOut), () -> {
return new OnCloseOutputStream(compression.compress(byteOut), () -> {
int mapFK = getMapFK(mapId);
int tileCompressionFK = getMapTileCompressionFK(compression);
@ -71,7 +71,7 @@ public OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IO
@Override
public OutputStream writeMeta(String mapId, String name) {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new WrappedOutputStream(byteOut, () -> {
return new OnCloseOutputStream(byteOut, () -> {
int mapFK = getMapFK(mapId);
recoveringConnection(connection -> {
executeUpdate(connection, this.dialect.writeMeta(),

View File

@ -32,7 +32,7 @@
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.storage.sql.dialect.DialectType;
import de.bluecolored.bluemap.core.storage.sql.dialect.Dialect;
import de.bluecolored.bluemap.core.util.WrappedOutputStream;
import de.bluecolored.bluemap.core.util.OnCloseOutputStream;
import org.apache.commons.dbcp2.*;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
@ -108,7 +108,7 @@ public OutputStream writeMapTile(String mapId, int lod, Vector2i tile) throws IO
Compression compression = lod == 0 ? this.hiresCompression : Compression.NONE;
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new WrappedOutputStream(compression.compress(byteOut), () -> {
return new OnCloseOutputStream(compression.compress(byteOut), () -> {
int mapFK = getMapFK(mapId);
int tileCompressionFK = getMapTileCompressionFK(compression);
@ -234,7 +234,7 @@ public void deleteMapTile(String mapId, int lod, Vector2i tile) throws IOExcepti
@Override
public OutputStream writeMeta(String mapId, String name) {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
return new WrappedOutputStream(byteOut, () -> {
return new OnCloseOutputStream(byteOut, () -> {
int mapFK = getMapFK(mapId);
recoveringConnection(connection -> {

View File

@ -40,7 +40,7 @@ public static OutputStream createFilepartOutputStream(final Path file) throws IO
final Path partFile = getPartFile(file);
FileHelper.createDirectories(partFile.getParent());
OutputStream os = Files.newOutputStream(partFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
return new WrappedOutputStream(os, () -> {
return new OnCloseOutputStream(os, () -> {
if (!Files.exists(partFile)) return;
FileHelper.createDirectories(file.getParent());
FileHelper.move(partFile, file);

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.world;
package de.bluecolored.bluemap.core.util;
import com.flowpowered.math.vector.Vector2i;

View File

@ -26,10 +26,15 @@
import de.bluecolored.bluemap.api.debug.DebugDump;
@DebugDump
public class Key {
import java.util.concurrent.ConcurrentHashMap;
private static final String MINECRAFT_NAMESPACE = "minecraft";
@DebugDump
public class Key implements Keyed {
private static final ConcurrentHashMap<String, String> STRING_INTERN_POOL = new ConcurrentHashMap<>();
public static final String MINECRAFT_NAMESPACE = "minecraft";
public static final String BLUEMAP_NAMESPACE = "bluemap";
private final String namespace;
private final String value;
@ -44,15 +49,15 @@ public Key(String formatted) {
value = formatted.substring(namespaceSeparator + 1);
}
this.namespace = namespace.intern();
this.value = value.intern();
this.formatted = (this.namespace + ":" + this.value).intern();
this.namespace = intern(namespace);
this.value = intern(value);
this.formatted = intern(this.namespace + ":" + this.value);
}
public Key(String namespace, String value) {
this.namespace = namespace.intern();
this.value = value.intern();
this.formatted = (this.namespace + ":" + this.value).intern();
this.namespace = intern(namespace);
this.value = intern(value);
this.formatted = intern(this.namespace + ":" + this.value);
}
public String getNamespace() {
@ -67,22 +72,52 @@ public String getFormatted() {
return formatted;
}
@Override
public Key getKey() {
return this;
}
@SuppressWarnings("StringEquality")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key that = (Key) o;
return getFormatted() == that.getFormatted();
return formatted == that.formatted;
}
@Override
public int hashCode() {
return getFormatted().hashCode();
return formatted.hashCode();
}
@Override
public String toString() {
return formatted;
}
public static Key parse(String formatted) {
return new Key(formatted);
}
public static Key parse(String formatted, String defaultNamespace) {
String namespace = defaultNamespace;
String value = formatted;
int namespaceSeparator = formatted.indexOf(':');
if (namespaceSeparator > 0) {
namespace = formatted.substring(0, namespaceSeparator);
value = formatted.substring(namespaceSeparator + 1);
}
return new Key(namespace, value);
}
/**
* Using our own function instead of {@link String#intern()} since the ConcurrentHashMap is much faster.
*/
protected static String intern(String string) {
String interned = STRING_INTERN_POOL.putIfAbsent(string, string);
return interned != null ? interned : string;
}
}

View File

@ -0,0 +1,7 @@
package de.bluecolored.bluemap.core.util;
public interface Keyed {
Key getKey();
}

View File

@ -24,34 +24,19 @@
*/
package de.bluecolored.bluemap.core.util;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public class WrappedInputStream extends InputStream {
public class OnCloseInputStream extends FilterInputStream {
private final InputStream in;
private final AutoCloseable onClose;
public WrappedInputStream(InputStream in, AutoCloseable onClose) {
this.in = in;
public OnCloseInputStream(InputStream in, AutoCloseable onClose) {
super(in);
this.onClose = onClose;
}
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] b) throws IOException {
return in.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
@Override
public void close() throws IOException {
IOException ioExcetion = null;

View File

@ -24,39 +24,19 @@
*/
package de.bluecolored.bluemap.core.util;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class WrappedOutputStream extends OutputStream {
public class OnCloseOutputStream extends FilterOutputStream {
private final OutputStream out;
private final AutoCloseable onClose;
public WrappedOutputStream(OutputStream out, AutoCloseable onClose) {
this.out = out;
public OnCloseOutputStream(OutputStream out, AutoCloseable onClose) {
super(out);
this.onClose = onClose;
}
@Override
public void write(int b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b) throws IOException {
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
}
@Override
public void flush() throws IOException {
out.flush();
}
@Override
public void close() throws IOException {
IOException ioExcetion = null;

View File

@ -0,0 +1,53 @@
package de.bluecolored.bluemap.core.util;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class Registry<T extends Keyed> {
private final ConcurrentHashMap<Key, T> entries;
public Registry() {
this.entries = new ConcurrentHashMap<>();
}
/**
* Registers a new entry, only if there is no entry with the same key registered already.
* Does nothing otherwise.
* @param entry The new entry to be added to this registry
* @return true if the entry has been added, false if there is already an entry with the same key registered
*/
public boolean register(T entry) {
Objects.requireNonNull(entry, "registry entry can not be null");
return entries.putIfAbsent(entry.getKey(), entry) != null;
}
/**
* Gets an entry from this registry for a key.
* @param key The key to search for
* @return The entry with the key, or null if there is no entry for this key
*/
public @Nullable T get(Key key) {
return entries.get(key);
}
/**
* Returns an unmodifiable set of all keys this registry contains entries for
*/
public Set<Key> keys() {
return Collections.unmodifiableSet(entries.keySet());
}
/**
* Returns an unmodifiable collection of entries in this registry
*/
public Collection<T> values() {
return Collections.unmodifiableCollection(entries.values());
}
}

View File

@ -197,8 +197,8 @@ public static final class Property implements Comparable<Property> {
private final String key, value;
public Property(String key, String value) {
this.key = key.intern();
this.value = value.intern();
this.key = intern(key);
this.value = intern(value);
}
@SuppressWarnings("StringEquality")

View File

@ -26,22 +26,50 @@
public interface Chunk {
boolean isGenerated();
Chunk EMPTY_CHUNK = new Chunk() {};
long getInhabitedTime();
default boolean isGenerated() {
return false;
}
BlockState getBlockState(int x, int y, int z);
default boolean hasLightData() {
return false;
}
LightData getLightData(int x, int y, int z, LightData target);
default long getInhabitedTime() {
return 0;
}
String getBiome(int x, int y, int z);
default BlockState getBlockState(int x, int y, int z) {
return BlockState.AIR;
}
int getMaxY(int x, int z);
default LightData getLightData(int x, int y, int z, LightData target) {
return target.set(0, 0);
}
int getMinY(int x, int z);
default String getBiome(int x, int y, int z) {
return Biome.DEFAULT.getFormatted();
}
int getWorldSurfaceY(int x, int z);
default int getMaxY(int x, int z) {
return 255;
}
int getOceanFloorY(int x, int z);
default int getMinY(int x, int z) {
return 0;
}
default boolean hasWorldSurfaceHeights() {
return false;
}
default int getWorldSurfaceY(int x, int z) { return 0; }
default boolean hasOceanFloorHeights() {
return false;
}
default int getOceanFloorY(int x, int z) { return 0; }
}

View File

@ -0,0 +1,30 @@
package de.bluecolored.bluemap.core.world;
@FunctionalInterface
public interface ChunkConsumer {
default boolean filter(int chunkX, int chunkZ, long lastModified) {
return true;
}
void accept(int chunkX, int chunkZ, Chunk chunk);
@FunctionalInterface
interface ListOnly extends ChunkConsumer {
void accept(int chunkX, int chunkZ, long lastModified);
@Override
default boolean filter(int chunkX, int chunkZ, long lastModified) {
accept(chunkX, chunkZ, lastModified);
return false;
}
@Override
default void accept(int chunkX, int chunkZ, Chunk chunk) {
throw new IllegalStateException("Should never be called.");
}
}
}

View File

@ -0,0 +1,82 @@
package de.bluecolored.bluemap.core.world;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Accessors;
public interface DimensionType {
DimensionType OVERWORLD = new Builtin(
true,
true,
false,
0f,
-64,
384,
null,
1.0
);
DimensionType OVERWORLD_CAVES = new Builtin(
true,
true,
true,
0,
-64,
384,
null,
1.0
);
DimensionType NETHER = new Builtin(
false,
false,
true,
0.1f,
0,
256,
6000L,
8.0
);
DimensionType END = new Builtin(
false,
false,
false,
0,
0,
256,
18000L,
1.0
);
boolean isNatural();
boolean hasSkylight();
boolean hasCeiling();
float getAmbientLight();
int getMinY();
int getHeight();
Long getFixedTime();
double getCoordinateScale();
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
class Builtin implements DimensionType {
private final boolean natural;
@Accessors(fluent = true) private final boolean hasSkylight;
@Accessors(fluent = true) private final boolean hasCeiling;
private final float ambientLight;
private final int minY;
private final int height;
private final Long fixedTime;
private final double coordinateScale;
}
}

View File

@ -1,72 +0,0 @@
/*
* 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.world;
public class EmptyChunk implements Chunk {
public static final Chunk INSTANCE = new EmptyChunk();
@Override
public boolean isGenerated() {
return false;
}
@Override
public long getInhabitedTime() {
return 0;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
return BlockState.AIR;
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
return target.set(0, 0);
}
@Override
public String getBiome(int x, int y, int z) {
return Biome.DEFAULT.getFormatted();
}
@Override
public int getMaxY(int x, int z) {
return 255;
}
@Override
public int getMinY(int x, int z) {
return 0;
}
@Override
public int getWorldSurfaceY(int x, int z) { return 0; }
@Override
public int getOceanFloorY(int x, int z) { return 0; }
}

View File

@ -24,34 +24,41 @@
*/
package de.bluecolored.bluemap.core.world;
import com.flowpowered.math.vector.Vector2i;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
public interface Region {
/**
* Returns a collection of all generated chunks.<br>
* <i>(Be aware that the collection is not cached and recollected each time from the world-files!)</i>
* Directly loads and returns the specified chunk.<br>
* (implementations should consider overriding this method for a faster implementation)
*/
default Collection<Vector2i> listChunks(){
return listChunks(0);
default Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
class SingleChunkConsumer implements ChunkConsumer {
private Chunk foundChunk = Chunk.EMPTY_CHUNK;
@Override
public boolean filter(int x, int z, long lastModified) {
return x == chunkX && z == chunkZ;
}
@Override
public void accept(int chunkX, int chunkZ, Chunk chunk) {
this.foundChunk = chunk;
}
}
SingleChunkConsumer singleChunkConsumer = new SingleChunkConsumer();
iterateAllChunks(singleChunkConsumer);
return singleChunkConsumer.foundChunk;
}
/**
* Returns a collection of all chunks that have been modified at or after the specified timestamp.<br>
* <i>(Be aware that the collection is not cached and recollected each time from the world-files!)</i>
* Iterates over all chunks in this region and first calls {@link ChunkConsumer#filter(int, int, long)}.<br>
* And if (any only if) that method returned <code>true</code>, the chunk will be loaded and {@link ChunkConsumer#accept(int, int, Chunk)}
* will be called with the loaded chunk.
* @param consumer the consumer choosing which chunks to load and accepting them
* @throws IOException if an IOException occurred trying to read the region
*/
Collection<Vector2i> listChunks(long modifiedSince);
default Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
return loadChunk(chunkX, chunkZ, false);
}
Chunk loadChunk(int chunkX, int chunkZ, boolean ignoreMissingLightData) throws IOException;
Path getRegionFile();
void iterateAllChunks(ChunkConsumer consumer) throws IOException;
}

View File

@ -26,29 +26,25 @@
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.util.Grid;
import java.nio.file.Path;
import java.util.Collection;
import java.util.UUID;
/**
* Represents a World on the Server<br>
* Represents a World on the Server.<br>
* This is usually one of the dimensions of a level.<br>
* <br>
* <i>The implementation of this class has to be thread-save!</i><br>
*/
public interface World {
Path getSaveFolder();
String getId();
String getName();
int getSkyLight();
Vector3i getSpawnPoint();
int getMaxY(int x, int z);
int getMinY(int x, int z);
DimensionType getDimensionType();
Grid getChunkGrid();
@ -57,7 +53,7 @@ public interface World {
/**
* Returns the {@link Chunk} on the specified block-position
*/
Chunk getChunkAtBlock(int x, int y, int z);
Chunk getChunkAtBlock(int x, int z);
/**
* Returns the {@link Chunk} on the specified chunk-position
@ -75,6 +71,11 @@ public interface World {
*/
Collection<Vector2i> listRegions();
/**
* Loads all chunks from the specified region into the chunk cache (if there is a cache)
*/
void preloadRegionChunks(int x, int z);
/**
* Invalidates the complete chunk cache (if there is a cache), so that every chunk has to be reloaded from disk
*/

View File

@ -22,7 +22,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.world;
package de.bluecolored.bluemap.core.world.block;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.World;
public class Block<T extends Block<T>> {
@ -98,22 +103,6 @@ public T copy(Block<?> source) {
return self();
}
/**
* copy with offset
*/
public T copy(Block<?> source, int dx, int dy, int dz) {
this.world = source.world;
this.x = source.x + dx;
this.y = source.y + dy;
this.z = source.z + dz;
this.chunk = null;
reset();
return self();
}
public World getWorld() {
return world;
}
@ -131,7 +120,7 @@ public int getZ() {
}
public Chunk getChunk() {
if (chunk == null) chunk = world.getChunkAtBlock(x, y, z);
if (chunk == null) chunk = world.getChunkAtBlock(x, z);
return chunk;
}

View File

@ -22,10 +22,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.world;
package de.bluecolored.bluemap.core.world.block;
import de.bluecolored.bluemap.core.map.hires.RenderSettings;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.world.World;
public class BlockNeighborhood<T extends BlockNeighborhood<T>> extends ExtendedBlock<T> {
@ -52,6 +53,19 @@ public BlockNeighborhood(ResourcePack resourcePack, RenderSettings renderSetting
init();
}
@Override
public T set(int x, int y, int z) {
return copy(getBlock(x, y, z));
}
@Override
public T set(World world, int x, int y, int z) {
if (getWorld() == world)
return copy(getBlock(x, y, z));
else
return super.set(world, x, y, z);
}
@Override
protected void reset() {
super.reset();
@ -67,25 +81,28 @@ private void init() {
}
public ExtendedBlock<?> getNeighborBlock(int dx, int dy, int dz) {
int i = neighborIndex(dx, dy, dz);
if (i == thisIndex()) return this;
return neighborhood[i].set(
getWorld(),
return getBlock(
getX() + dx,
getY() + dy,
getZ() + dz
);
}
private ExtendedBlock<?> getBlock(int x, int y, int z) {
int i = index(x, y, z);
if (i == thisIndex()) return this;
return neighborhood[i].set(getWorld(), x, y, z);
}
private int thisIndex() {
if (thisIndex == -1) thisIndex = neighborIndex(0, 0, 0);
if (thisIndex == -1) thisIndex = index(getX(), getY(), getZ());
return thisIndex;
}
private int neighborIndex(int dx, int dy, int dz) {
return ((getX() + dx) & DIAMETER_MASK) * DIAMETER_SQUARED +
((getY() + dy) & DIAMETER_MASK) * DIAMETER +
((getZ() + dz) & DIAMETER_MASK);
private int index(int x, int y, int z) {
return (x & DIAMETER_MASK) * DIAMETER_SQUARED +
(y & DIAMETER_MASK) * DIAMETER +
(z & DIAMETER_MASK);
}
}

View File

@ -22,20 +22,23 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.world;
package de.bluecolored.bluemap.core.world.block;
import de.bluecolored.bluemap.core.map.hires.RenderSettings;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.world.*;
import java.util.Objects;
public class ExtendedBlock<T extends ExtendedBlock<T>> extends Block<T> {
private final ResourcePack resourcePack;
private final RenderSettings renderSettings;
private BlockProperties properties;
private Biome biome;
private boolean insideRenderBoundsCalculated, insideRenderBounds;
private boolean isCaveCalculated, isCave;
public ExtendedBlock(ResourcePack resourcePack, RenderSettings renderSettings, World world, int x, int y, int z) {
super(world, x, y, z);
@ -51,6 +54,22 @@ protected void reset() {
this.biome = null;
this.insideRenderBoundsCalculated = false;
this.isCaveCalculated = false;
}
public T copy(ExtendedBlock<?> source) {
super.copy(source);
this.properties = source.properties;
this.biome = source.biome;
this.insideRenderBoundsCalculated = source.insideRenderBoundsCalculated;
this.insideRenderBounds = source.insideRenderBounds;
this.isCaveCalculated = source.isCaveCalculated;
this.isCave = source.isCave;
return self();
}
@Override
@ -62,7 +81,7 @@ public BlockState getBlockState() {
@Override
public LightData getLightData() {
LightData ld = super.getLightData();
if (renderSettings.isRenderEdges() && !isInsideRenderBounds()) ld.set(getWorld().getSkyLight(), ld.getBlockLight());
if (renderSettings.isRenderEdges() && !isInsideRenderBounds()) ld.set(getWorld().getDimensionType().hasSkylight() ? 16 : 0, ld.getBlockLight());
return ld;
}
@ -90,6 +109,20 @@ public boolean isInsideRenderBounds() {
return insideRenderBounds;
}
public boolean isRemoveIfCave() {
if (!isCaveCalculated) {
isCave = getY() < renderSettings.getRemoveCavesBelowY() &&
(
!getChunk().hasOceanFloorHeights() ||
getY() < getChunk().getOceanFloorY(getX(), getZ()) +
renderSettings.getCaveDetectionOceanFloor()
);
isCaveCalculated = true;
}
return isCave;
}
public ResourcePack getResourcePack() {
return resourcePack;
}

View File

@ -22,9 +22,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.mca;
package de.bluecolored.bluemap.core.world.mca;
public class MCAMath {
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.mca.data.BlockStateDeserializer;
import de.bluecolored.bluemap.core.world.mca.data.KeyDeserializer;
import de.bluecolored.bluenbt.BlueNBT;
public class MCAUtil {
public static final BlueNBT BLUENBT = new BlueNBT();
static {
BLUENBT.register(TypeToken.get(BlockState.class), new BlockStateDeserializer());
BLUENBT.register(TypeToken.get(Key.class), new KeyDeserializer());
}
/**
* Having a long array where each long contains as many values as fit in it without overflowing, returning the "valueIndex"-th value when each value has "bitsPerValue" bits.
@ -34,6 +47,7 @@ public static long getValueFromLongArray(long[] data, int valueIndex, int bitsPe
int longIndex = valueIndex / valuesPerLong;
int bitIndex = (valueIndex % valuesPerLong) * bitsPerValue;
if (longIndex >= data.length) return 0;
long value = data[longIndex] >>> bitIndex;
return value & (0xFFFFFFFFFFFFFFFFL >>> -bitsPerValue);
@ -42,16 +56,18 @@ public static long getValueFromLongArray(long[] data, int valueIndex, int bitsPe
/**
* Treating the long array "data" as a continuous stream of bits, returning the "valueIndex"-th value when each value has "bitsPerValue" bits.
*/
@SuppressWarnings("ShiftOutOfRange")
public static long getValueFromLongStream(long[] data, int valueIndex, int bitsPerValue) {
int bitIndex = valueIndex * bitsPerValue;
int firstLong = bitIndex >> 6; // index / 64
int bitoffset = bitIndex & 0x3F; // Math.floorMod(index, 64)
int bitOffset = bitIndex & 0x3F; // Math.floorMod(index, 64)
long value = data[firstLong] >>> bitoffset;
if (firstLong >= data.length) return 0;
long value = data[firstLong] >>> bitOffset;
if (bitoffset > 0 && firstLong + 1 < data.length) {
if (bitOffset > 0 && firstLong + 1 < data.length) {
long value2 = data[firstLong + 1];
value2 = value2 << -bitoffset;
value2 = value2 << -bitOffset;
value = value | value2;
}
@ -63,12 +79,12 @@ public static long getValueFromLongStream(long[] data, int valueIndex, int bitsP
* The value is treated as an unsigned byte.
*/
public static int getByteHalf(int value, boolean largeHalf) {
value = value & 0xFF;
if (largeHalf) {
value = value >> 4;
if (largeHalf) return value >> 4 & 0xF;
return value & 0xF;
}
value = value & 0xF;
return value;
public static int ceilLog2(int n) {
return Integer.SIZE - Integer.numberOfLeadingZeros(n - 1);
}
}

View File

@ -0,0 +1,275 @@
package de.bluecolored.bluemap.core.world.mca;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3i;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.datapack.DataPack;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.mca.chunk.ChunkLoader;
import de.bluecolored.bluemap.core.world.mca.data.LevelData;
import de.bluecolored.bluemap.core.world.mca.region.RegionType;
import lombok.Getter;
import lombok.ToString;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
@Getter
@ToString
@DebugDump
public class MCAWorld implements World {
private static final Grid CHUNK_GRID = new Grid(16);
private static final Grid REGION_GRID = new Grid(32).multiply(CHUNK_GRID);
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
private final String id;
private final Path worldFolder;
private final Key dimension;
private final LevelData levelData;
private final DataPack dataPack;
private final DimensionType dimensionType;
private final Vector3i spawnPoint;
private final Path dimensionFolder;
private final Path regionFolder;
private final ChunkLoader chunkLoader = new ChunkLoader();
private final LoadingCache<Vector2i, Region> regionCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.maximumSize(64)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::loadRegion);
private final LoadingCache<Vector2i, Chunk> chunkCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.maximumSize(10240) // 10 regions worth of chunks
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::loadChunk);
private MCAWorld(Path worldFolder, Key dimension, LevelData levelData, DataPack dataPack) {
this.id = id(worldFolder, dimension);
this.worldFolder = worldFolder;
this.dimension = dimension;
this.levelData = levelData;
this.dataPack = dataPack;
LevelData.Dimension dimensionData = levelData.getData().getWorldGenSettings().getDimensions().get(dimension.getFormatted());
if (dimensionData == null) {
if (DataPack.DIMENSION_OVERWORLD.equals(dimension)) dimensionData = new LevelData.Dimension(DataPack.DIMENSION_TYPE_OVERWORLD.getFormatted());
else if (DataPack.DIMENSION_THE_NETHER.equals(dimension)) dimensionData = new LevelData.Dimension(DataPack.DIMENSION_TYPE_THE_NETHER.getFormatted());
else if (DataPack.DIMENSION_THE_END.equals(dimension)) dimensionData = new LevelData.Dimension(DataPack.DIMENSION_TYPE_THE_END.getFormatted());
else {
Logger.global.logWarning("The level-data does not contain any dimension with the id '" + dimension +
"', using fallback.");
dimensionData = new LevelData.Dimension();
}
}
DimensionType dimensionType = dataPack.getDimensionType(new Key(dimensionData.getType()));
if (dimensionType == null) {
Logger.global.logWarning("The data-pack for world '" + worldFolder +
"' does not contain any dimension-type with the id '" + dimensionData.getType() + "', using fallback.");
dimensionType = DimensionType.OVERWORLD;
}
this.dimensionType = dimensionType;
this.spawnPoint = new Vector3i(
levelData.getData().getSpawnX(),
levelData.getData().getSpawnY(),
levelData.getData().getSpawnZ()
);
this.dimensionFolder = resolveDimensionFolder(worldFolder, dimension);
this.regionFolder = dimensionFolder.resolve("region");
}
@Override
public String getName() {
return levelData.getData().getLevelName();
}
@Override
public Grid getChunkGrid() {
return CHUNK_GRID;
}
@Override
public Grid getRegionGrid() {
return REGION_GRID;
}
@Override
public Chunk getChunkAtBlock(int x, int z) {
return getChunk(x >> 4, z >> 4);
}
@Override
public Chunk getChunk(int x, int z) {
return getChunk(VECTOR_2_I_CACHE.get(x, z));
}
private Chunk getChunk(Vector2i pos) {
return chunkCache.get(pos);
}
@Override
public Region getRegion(int x, int z) {
return getRegion(VECTOR_2_I_CACHE.get(x, z));
}
private Region getRegion(Vector2i pos) {
return regionCache.get(pos);
}
@Override
public Collection<Vector2i> listRegions() {
File[] regionFiles = getRegionFolder().toFile().listFiles();
if (regionFiles == null) return Collections.emptyList();
List<Vector2i> regions = new ArrayList<>(regionFiles.length);
for (File file : regionFiles) {
if (RegionType.forFileName(file.getName()) == null) continue;
if (file.length() <= 0) continue;
try {
String[] filenameParts = file.getName().split("\\.");
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
regions.add(new Vector2i(rX, rZ));
} catch (NumberFormatException ignore) {}
}
return regions;
}
@Override
public void preloadRegionChunks(int x, int z) {
try {
getRegion(x, z).iterateAllChunks((cx, cz, chunk) -> {
Vector2i chunkPos = VECTOR_2_I_CACHE.get(cx, cz);
chunkCache.put(chunkPos, chunk);
});
} catch (IOException ex) {
Logger.global.logDebug("Unexpected exception trying to load preload region (x:" + x + ", z:" + z + "):" + ex);
}
}
@Override
public void invalidateChunkCache() {
chunkCache.invalidateAll();
}
@Override
public void invalidateChunkCache(int x, int z) {
chunkCache.invalidate(VECTOR_2_I_CACHE.get(x, z));
}
@Override
public void cleanUpChunkCache() {
chunkCache.cleanUp();
}
private Region loadRegion(Vector2i regionPos) {
return loadRegion(regionPos.getX(), regionPos.getY());
}
private Region loadRegion(int x, int z) {
return RegionType.loadRegion(this, getRegionFolder(), x, z);
}
private Chunk loadChunk(Vector2i chunkPos) {
return loadChunk(chunkPos.getX(), chunkPos.getY());
}
private Chunk loadChunk(int x, int z) {
final int tries = 3;
final int tryInterval = 1000;
Exception loadException = null;
for (int i = 0; i < tries; i++) {
try {
return getRegion(x >> 5, z >> 5)
.loadChunk(x, z);
} catch (IOException | RuntimeException e) {
if (loadException != null) e.addSuppressed(loadException);
loadException = e;
if (i + 1 < tries) {
try {
Thread.sleep(tryInterval);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
Logger.global.logDebug("Unexpected exception trying to load chunk (x:" + x + ", z:" + z + "):" + loadException);
return Chunk.EMPTY_CHUNK;
}
public static MCAWorld load(Path worldFolder, Key dimension) throws IOException, InterruptedException {
// load level.dat
Path levelFile = worldFolder.resolve("level.dat");
InputStream levelFileIn = new GZIPInputStream(new BufferedInputStream(Files.newInputStream(levelFile)));
LevelData levelData = MCAUtil.BLUENBT.read(levelFileIn, LevelData.class);
// load datapacks
DataPack dataPack = new DataPack();
Path dataPackFolder = worldFolder.resolve("datapacks");
if (Files.exists(dataPackFolder)) {
List<Path> roots;
try (var stream = Files.list(dataPackFolder)) {
roots = stream
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
}
for (Path root : roots) {
dataPack.load(root);
}
}
dataPack.bake();
// create world
return new MCAWorld(worldFolder, dimension, levelData, dataPack);
}
public static String id(Path worldFolder, Key dimension) {
worldFolder = worldFolder.toAbsolutePath().normalize();
Path workingDir = Path.of("").toAbsolutePath().normalize();
if (worldFolder.startsWith(workingDir))
worldFolder = workingDir.relativize(worldFolder);
return "MCA#" + worldFolder + "#" + dimension.getFormatted();
}
public static Path resolveDimensionFolder(Path worldFolder, Key dimension) {
if (DataPack.DIMENSION_OVERWORLD.equals(dimension)) return worldFolder;
if (DataPack.DIMENSION_THE_NETHER.equals(dimension)) return worldFolder.resolve("DIM-1");
if (DataPack.DIMENSION_THE_END.equals(dimension)) return worldFolder.resolve("DIM1");
return worldFolder.resolve("dimensions").resolve(dimension.getNamespace()).resolve(dimension.getValue());
}
}

View File

@ -0,0 +1,120 @@
package de.bluecolored.bluemap.core.world.mca;
public class PackedIntArrayAccess {
// magic constants for fast division
private static final int[] DIVISION_MAGIC = new int[]{
// <editor-fold defaultstate="collapsed" desc="Division-Magic Constants">
-1, -1, 0,
Integer.MIN_VALUE, 0, 0,
1431655765, 1431655765, 0,
Integer.MIN_VALUE, 0, 1,
858993459, 858993459, 0,
715827882, 715827882, 0,
613566756, 613566756, 0,
Integer.MIN_VALUE, 0, 2,
477218588, 477218588, 0,
429496729, 429496729, 0,
390451572, 390451572, 0,
357913941, 357913941, 0,
330382099, 330382099, 0,
306783378, 306783378, 0,
286331153, 286331153, 0,
Integer.MIN_VALUE, 0, 3,
252645135, 252645135, 0,
238609294, 238609294, 0,
226050910, 226050910, 0,
214748364, 214748364, 0,
204522252, 204522252, 0,
195225786, 195225786, 0,
186737708, 186737708, 0,
178956970, 178956970, 0,
171798691, 171798691, 0,
165191049, 165191049, 0,
159072862, 159072862, 0,
153391689, 153391689, 0,
148102320, 148102320, 0,
143165576, 143165576, 0,
138547332, 138547332, 0,
Integer.MIN_VALUE, 0, 4,
130150524, 130150524, 0,
126322567, 126322567, 0,
122713351, 122713351, 0,
119304647, 119304647, 0,
116080197, 116080197, 0,
113025455, 113025455, 0,
110127366, 110127366, 0,
107374182, 107374182, 0,
104755299, 104755299, 0,
102261126, 102261126, 0,
99882960, 99882960, 0,
97612893, 97612893, 0,
95443717, 95443717, 0,
93368854, 93368854, 0,
91382282, 91382282, 0,
89478485, 89478485, 0,
87652393, 87652393, 0,
85899345, 85899345, 0,
84215045, 84215045, 0,
82595524, 82595524, 0,
81037118, 81037118, 0,
79536431, 79536431, 0,
78090314, 78090314, 0,
76695844, 76695844, 0,
75350303, 75350303, 0,
74051160, 74051160, 0,
72796055, 72796055, 0,
71582788, 71582788, 0,
70409299, 70409299, 0,
69273666, 69273666, 0,
68174084, 68174084, 0,
Integer.MIN_VALUE, 0, 5
// </editor-fold>
};
private final int bitsPerElement;
private final long[] data;
private final int elementsPerLong, indexShift;
private final long maxValue, indexScale, indexOffset;
public PackedIntArrayAccess(long[] data, int elementCount) {
this(Math.max(data.length * Long.SIZE / elementCount, 1), data);
}
public PackedIntArrayAccess(int bitsPerElement, long[] data) {
this.bitsPerElement = bitsPerElement;
this.data = data;
this.maxValue = (1L << this.bitsPerElement) - 1L;
this.elementsPerLong = 64 / this.bitsPerElement;
int i = 3 * (this.elementsPerLong - 1);
this.indexScale = Integer.toUnsignedLong(DIVISION_MAGIC[i]);
this.indexOffset = Integer.toUnsignedLong(DIVISION_MAGIC[i + 1]);
this.indexShift = DIVISION_MAGIC[i + 2] + 32;
}
public int get(int i) {
int storageIndex = this.storageIndex(i);
if (storageIndex >= this.data.length) return 0;
long l = this.data[storageIndex];
int offset = (i - storageIndex * this.elementsPerLong) * this.bitsPerElement;
return (int)(l >> offset & this.maxValue);
}
private int storageIndex(int i) {
// this is the same as doing: floor(i / elementsPerLong)
return (int) ((long) i * this.indexScale + this.indexOffset >> this.indexShift);
}
public int getCapacity() {
return data.length * elementsPerLong;
}
public boolean isCorrectSize(int expectedSize) {
int capacity = getCapacity();
return expectedSize <= capacity && expectedSize + elementsPerLong > capacity;
}
}

View File

@ -0,0 +1,79 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.function.BiFunction;
public class ChunkLoader {
// sorted list of chunk-versions, loaders at the start of the list are preferred over loaders at the end
private static final List<ChunkVersionLoader<?>> CHUNK_VERSION_LOADERS = List.of(
new ChunkVersionLoader<>(Chunk_1_18.Data.class, Chunk_1_18::new, 2844),
new ChunkVersionLoader<>(Chunk_1_16.Data.class, Chunk_1_16::new, 2500),
new ChunkVersionLoader<>(Chunk_1_15.Data.class, Chunk_1_15::new, 2200),
new ChunkVersionLoader<>(Chunk_1_13.Data.class, Chunk_1_13::new, 0)
);
private ChunkVersionLoader<?> lastUsedLoader = CHUNK_VERSION_LOADERS.get(0);
public MCAChunk load(MCARegion region, byte[] data, int offset, int length, Compression compression) throws IOException {
InputStream in = new ByteArrayInputStream(data, offset, length);
in.mark(-1);
// try last used version
ChunkVersionLoader<?> usedLoader = lastUsedLoader;
MCAChunk chunk;
try (InputStream decompressedIn = new BufferedInputStream(compression.decompress(in))) {
chunk = usedLoader.load(region, decompressedIn);
}
// check version and reload chunk if the wrong loader has been used and a better one has been found
ChunkVersionLoader<?> actualLoader = findBestLoaderForVersion(chunk.getDataVersion());
if (actualLoader != null && usedLoader != actualLoader) {
in.reset(); // reset read position
try (InputStream decompressedIn = new BufferedInputStream(compression.decompress(in))) {
chunk = actualLoader.load(region, decompressedIn);
}
lastUsedLoader = actualLoader;
}
return chunk;
}
private @Nullable ChunkVersionLoader<?> findBestLoaderForVersion(int version) {
for (ChunkVersionLoader<?> loader : CHUNK_VERSION_LOADERS) {
if (loader.mightSupport(version)) return loader;
}
return null;
}
@RequiredArgsConstructor
@Getter
private static class ChunkVersionLoader<D extends MCAChunk.Data> {
private final Class<D> dataType;
private final BiFunction<MCARegion, D, MCAChunk> constructor;
private final int dataVersion;
public MCAChunk load(MCARegion region, InputStream in) throws IOException {
D data = MCAUtil.BLUENBT.read(in, dataType);
return mightSupport(data.getDataVersion()) ? constructor.apply(region, data) : new MCAChunk(region, data) {};
}
public boolean mightSupport(int dataVersion) {
return dataVersion >= this.dataVersion;
}
}
}

View File

@ -0,0 +1,274 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
import de.bluecolored.bluenbt.NBTName;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
public class Chunk_1_13 extends MCAChunk {
private static final Level EMPTY_LEVEL = new Level();
private static final HeightmapsData EMPTY_HEIGHTMAPS_DATA = new HeightmapsData();
private static final Key STATUS_EMPTY = new Key("minecraft", "empty");
private static final Key STATUS_FULL = new Key("minecraft", "full");
private static final Key STATUS_FULLCHUNK = new Key("minecraft", "fullchunk");
private static final Key STATUS_POSTPROCESSED = new Key("minecraft", "postprocessed");
private final boolean generated;
private final boolean hasLightData;
private final long inhabitedTime;
private final int skyLight;
private final boolean hasWorldSurfaceHeights;
private final long[] worldSurfaceHeights;
private final boolean hasOceanFloorHeights;
private final long[] oceanFloorHeights;
private final Section[] sections;
private final int sectionMin, sectionMax;
final int[] biomes;
public Chunk_1_13(MCARegion region, Data data) {
super(region, data);
Level level = data.level;
this.generated = !STATUS_EMPTY.equals(level.status);
this.hasLightData =
STATUS_FULL.equals(level.status) ||
STATUS_FULLCHUNK.equals(level.status) ||
STATUS_POSTPROCESSED.equals(level.status);
this.inhabitedTime = level.inhabitedTime;
DimensionType dimensionType = getRegion().getWorld().getDimensionType();
this.skyLight = dimensionType.hasSkylight() ? 16 : 0;
this.worldSurfaceHeights = level.heightmaps.worldSurface;
this.oceanFloorHeights = level.heightmaps.oceanFloor;
this.hasWorldSurfaceHeights = this.worldSurfaceHeights.length >= 36;
this.hasOceanFloorHeights = this.oceanFloorHeights.length >= 36;
this.biomes = level.biomes;
SectionData[] sectionsData = level.sections;
if (sectionsData != null && sectionsData.length > 0) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
// find section min/max y
for (SectionData sectionData : sectionsData) {
int y = sectionData.getY();
if (min > y) min = y;
if (max < y) max = y;
}
// load sections into ordered array
this.sections = new Section[1 + max - min];
for (SectionData sectionData : sectionsData) {
Section section = new Section(sectionData);
int y = section.getSectionY();
if (min > y) min = y;
if (max < y) max = y;
this.sections[section.sectionY - min] = section;
}
this.sectionMin = min;
this.sectionMax = max;
} else {
this.sections = new Section[0];
this.sectionMin = 0;
this.sectionMax = 0;
}
}
@Override
public boolean isGenerated() {
return generated;
}
@Override
public boolean hasLightData() {
return hasLightData;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
Section section = getSection(y >> 4);
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public String getBiome(int x, int y, int z) {
if (this.biomes.length < 256) return Biome.DEFAULT.getFormatted();
int biomeIntIndex = (z & 0xF) << 4 | x & 0xF;
return LegacyBiomes.idFor(biomes[biomeIntIndex]);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLightData) return target.set(skyLight, 0);
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return (sectionY < sectionMin) ? target.set(0, 0) : target.set(skyLight, 0);
return section.getLightData(x, y, z, target);
}
@Override
public int getMinY(int x, int z) {
return sectionMin * 16;
}
@Override
public int getMaxY(int x, int z) {
return sectionMax * 16 + 15;
}
@Override
public boolean hasWorldSurfaceHeights() {
return hasWorldSurfaceHeights;
}
@Override
public int getWorldSurfaceY(int x, int z) {
return (int) MCAUtil.getValueFromLongStream(
worldSurfaceHeights,
(z & 0xF) << 4 | x & 0xF,
9
);
}
@Override
public boolean hasOceanFloorHeights() {
return hasOceanFloorHeights;
}
@Override
public int getOceanFloorY(int x, int z) {
return (int) MCAUtil.getValueFromLongStream(
oceanFloorHeights,
(z & 0xF) << 4 | x & 0xF,
9
);
}
private @Nullable Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
return this.sections[y];
}
protected static class Section {
private final int sectionY;
private final BlockState[] blockPalette;
private final long[] blocks;
private final byte[] blockLight;
private final byte[] skyLight;
private final int bitsPerBlock;
public Section(SectionData sectionData) {
this.sectionY = sectionData.y;
this.blockPalette = sectionData.palette;
this.blocks = sectionData.blockStates;
this.blockLight = sectionData.getBlockLight();
this.skyLight = sectionData.getSkyLight();
this.bitsPerBlock = this.blocks.length >> 6; // available longs * 64 (bits per long) / 4096 (blocks per section) (floored result)
}
public BlockState getBlockState(int x, int y, int z) {
if (blockPalette.length == 1) return blockPalette[0];
if (blockPalette.length == 0) return BlockState.AIR;
int id = (int) MCAUtil.getValueFromLongStream(
blocks,
(y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF,
bitsPerBlock
);
if (id >= blockPalette.length) {
Logger.global.noFloodWarning("palette-warning", "Got block-palette id " + id + " but palette has size of " + blockPalette.length + "! (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return blockPalette[id];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
int blockByteIndex = (y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF;
int blockHalfByteIndex = blockByteIndex >> 1;
boolean largeHalf = (blockByteIndex & 0x1) != 0;
return target.set(
this.skyLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
public int getSectionY() {
return sectionY;
}
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
public static class Data extends MCAChunk.Data {
private Level level = EMPTY_LEVEL;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
public static class Level {
private Key status = STATUS_EMPTY;
private long inhabitedTime = 0;
private HeightmapsData heightmaps = EMPTY_HEIGHTMAPS_DATA;
private SectionData @Nullable [] sections = null;
private int[] biomes = EMPTY_INT_ARRAY;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class HeightmapsData {
@NBTName("WORLD_SURFACE") private long[] worldSurface = EMPTY_LONG_ARRAY;
@NBTName("OCEAN_FLOOR") private long[] oceanFloor = EMPTY_LONG_ARRAY;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class SectionData {
private int y = 0;
private byte[] blockLight = EMPTY_BYTE_ARRAY;
private byte[] skyLight = EMPTY_BYTE_ARRAY;
private BlockState[] palette = EMPTY_BLOCKSTATE_ARRAY;
private long[] blockStates = EMPTY_LONG_ARRAY;
}
}

View File

@ -0,0 +1,25 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
public class Chunk_1_15 extends Chunk_1_13 {
public Chunk_1_15(MCARegion region, Data data) {
super(region, data);
}
@Override
public String getBiome(int x, int y, int z) {
if (this.biomes.length < 16) return Biome.DEFAULT.getFormatted();
int biomeIntIndex = (y & 0b1100) << 2 | z & 0b1100 | (x & 0b1100) >> 2;
// shift y up/down if not in range
if (biomeIntIndex >= biomes.length) biomeIntIndex -= (((biomeIntIndex - biomes.length) >> 4) + 1) * 16;
if (biomeIntIndex < 0) biomeIntIndex -= (biomeIntIndex >> 4) * 16;
return LegacyBiomes.idFor(biomes[biomeIntIndex]);
}
}

View File

@ -0,0 +1,262 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.PackedIntArrayAccess;
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
import de.bluecolored.bluenbt.NBTName;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
public class Chunk_1_16 extends MCAChunk {
private static final Level EMPTY_LEVEL = new Level();
private static final HeightmapsData EMPTY_HEIGHTMAPS_DATA = new HeightmapsData();
private static final Key STATUS_EMPTY = new Key("minecraft", "empty");
private static final Key STATUS_FULL = new Key("minecraft", "full");
private final boolean generated;
private final boolean hasLightData;
private final long inhabitedTime;
private final int skyLight;
private final boolean hasWorldSurfaceHeights;
private final PackedIntArrayAccess worldSurfaceHeights;
private final boolean hasOceanFloorHeights;
private final PackedIntArrayAccess oceanFloorHeights;
private final Section[] sections;
private final int sectionMin, sectionMax;
private final int[] biomes;
public Chunk_1_16(MCARegion region, Data data) {
super(region, data);
Level level = data.level;
this.generated = !STATUS_EMPTY.equals(level.status);
this.hasLightData = STATUS_FULL.equals(level.status);
this.inhabitedTime = level.inhabitedTime;
DimensionType dimensionType = getRegion().getWorld().getDimensionType();
this.skyLight = dimensionType.hasSkylight() ? 16 : 0;
int worldHeight = dimensionType.getHeight();
int bitsPerHeightmapElement = MCAUtil.ceilLog2(worldHeight + 1);
this.worldSurfaceHeights = new PackedIntArrayAccess(bitsPerHeightmapElement, level.heightmaps.worldSurface);
this.oceanFloorHeights = new PackedIntArrayAccess(bitsPerHeightmapElement, level.heightmaps.oceanFloor);
this.hasWorldSurfaceHeights = this.worldSurfaceHeights.isCorrectSize(VALUES_PER_HEIGHTMAP);
this.hasOceanFloorHeights = this.oceanFloorHeights.isCorrectSize(VALUES_PER_HEIGHTMAP);
this.biomes = level.biomes;
SectionData[] sectionsData = level.sections;
if (sectionsData != null && sectionsData.length > 0) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
// find section min/max y
for (SectionData sectionData : sectionsData) {
int y = sectionData.getY();
if (min > y) min = y;
if (max < y) max = y;
}
// load sections into ordered array
this.sections = new Section[1 + max - min];
for (SectionData sectionData : sectionsData) {
Section section = new Section(sectionData);
int y = section.getSectionY();
if (min > y) min = y;
if (max < y) max = y;
this.sections[section.sectionY - min] = section;
}
this.sectionMin = min;
this.sectionMax = max;
} else {
this.sections = new Section[0];
this.sectionMin = 0;
this.sectionMax = 0;
}
}
@Override
public boolean isGenerated() {
return generated;
}
@Override
public boolean hasLightData() {
return hasLightData;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
Section section = getSection(y >> 4);
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public String getBiome(int x, int y, int z) {
if (this.biomes.length < 16) return Biome.DEFAULT.getFormatted();
int biomeIntIndex = (y & 0b1100) << 2 | z & 0b1100 | (x & 0b1100) >> 2;
// shift y up/down if not in range
if (biomeIntIndex >= biomes.length) biomeIntIndex -= (((biomeIntIndex - biomes.length) >> 4) + 1) * 16;
if (biomeIntIndex < 0) biomeIntIndex -= (biomeIntIndex >> 4) * 16;
return LegacyBiomes.idFor(biomes[biomeIntIndex]);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLightData) return target.set(skyLight, 0);
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return (sectionY < sectionMin) ? target.set(0, 0) : target.set(skyLight, 0);
return section.getLightData(x, y, z, target);
}
@Override
public int getMinY(int x, int z) {
return sectionMin * 16;
}
@Override
public int getMaxY(int x, int z) {
return sectionMax * 16 + 15;
}
@Override
public boolean hasWorldSurfaceHeights() {
return hasWorldSurfaceHeights;
}
@Override
public int getWorldSurfaceY(int x, int z) {
return worldSurfaceHeights.get((z & 0xF) << 4 | x & 0xF);
}
@Override
public boolean hasOceanFloorHeights() {
return hasOceanFloorHeights;
}
@Override
public int getOceanFloorY(int x, int z) {
return oceanFloorHeights.get((z & 0xF) << 4 | x & 0xF);
}
private @Nullable Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
return this.sections[y];
}
protected static class Section {
private final int sectionY;
private final BlockState[] blockPalette;
private final PackedIntArrayAccess blocks;
private final byte[] blockLight;
private final byte[] skyLight;
public Section(SectionData sectionData) {
this.sectionY = sectionData.y;
this.blockPalette = sectionData.palette;
this.blocks = new PackedIntArrayAccess(sectionData.blockStates, BLOCKS_PER_SECTION);
this.blockLight = sectionData.getBlockLight();
this.skyLight = sectionData.getSkyLight();
}
public BlockState getBlockState(int x, int y, int z) {
if (blockPalette.length == 1) return blockPalette[0];
if (blockPalette.length == 0) return BlockState.AIR;
int id = blocks.get((y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF);
if (id >= blockPalette.length) {
Logger.global.noFloodWarning("palette-warning", "Got block-palette id " + id + " but palette has size of " + blockPalette.length + "! (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return blockPalette[id];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
int blockByteIndex = (y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF;
int blockHalfByteIndex = blockByteIndex >> 1; // blockByteIndex / 2
boolean largeHalf = (blockByteIndex & 0x1) != 0; // (blockByteIndex % 2) == 0
return target.set(
this.skyLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
public int getSectionY() {
return sectionY;
}
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
public static class Data extends MCAChunk.Data {
private Level level = EMPTY_LEVEL;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
public static class Level {
private Key status = STATUS_EMPTY;
private long inhabitedTime = 0;
private HeightmapsData heightmaps = EMPTY_HEIGHTMAPS_DATA;
private SectionData @Nullable [] sections = null;
private int[] biomes = EMPTY_INT_ARRAY;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class HeightmapsData {
@NBTName("WORLD_SURFACE") private long[] worldSurface = EMPTY_LONG_ARRAY;
@NBTName("OCEAN_FLOOR") private long[] oceanFloor = EMPTY_LONG_ARRAY;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class SectionData {
private int y = 0;
private byte[] blockLight = EMPTY_BYTE_ARRAY;
private byte[] skyLight = EMPTY_BYTE_ARRAY;
private BlockState[] palette = EMPTY_BLOCKSTATE_ARRAY;
private long[] blockStates = EMPTY_LONG_ARRAY;
}
}

View File

@ -0,0 +1,279 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.LightData;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.PackedIntArrayAccess;
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
import de.bluecolored.bluenbt.NBTName;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
public class Chunk_1_18 extends MCAChunk {
private static final BlockStatesData EMPTY_BLOCKSTATESDATA = new BlockStatesData();
private static final BiomesData EMPTY_BIOMESDATA = new BiomesData();
private static final HeightmapsData EMPTY_HEIGHTMAPS_DATA = new HeightmapsData();
private static final Key STATUS_EMPTY = new Key("minecraft", "empty");
private static final Key STATUS_FULL = new Key("minecraft", "full");
private final boolean generated;
private final boolean hasLightData;
private final long inhabitedTime;
private final int skyLight;
private final int worldMinY;
private final boolean hasWorldSurfaceHeights;
private final PackedIntArrayAccess worldSurfaceHeights;
private final boolean hasOceanFloorHeights;
private final PackedIntArrayAccess oceanFloorHeights;
private final Section[] sections;
private final int sectionMin, sectionMax;
public Chunk_1_18(MCARegion region, Data data) {
super(region, data);
this.generated = !STATUS_EMPTY.equals(data.status);
this.hasLightData = STATUS_FULL.equals(data.status);
this.inhabitedTime = data.inhabitedTime;
DimensionType dimensionType = getRegion().getWorld().getDimensionType();
this.worldMinY = dimensionType.getMinY();
this.skyLight = dimensionType.hasSkylight() ? 16 : 0;
int worldHeight = dimensionType.getHeight();
int bitsPerHeightmapElement = MCAUtil.ceilLog2(worldHeight + 1);
this.worldSurfaceHeights = new PackedIntArrayAccess(bitsPerHeightmapElement, data.heightmaps.worldSurface);
this.oceanFloorHeights = new PackedIntArrayAccess(bitsPerHeightmapElement, data.heightmaps.oceanFloor);
this.hasWorldSurfaceHeights = this.worldSurfaceHeights.isCorrectSize(VALUES_PER_HEIGHTMAP);
this.hasOceanFloorHeights = this.oceanFloorHeights.isCorrectSize(VALUES_PER_HEIGHTMAP);
SectionData[] sectionsData = data.sections;
if (sectionsData != null && sectionsData.length > 0) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
// find section min/max y
for (SectionData sectionData : sectionsData) {
int y = sectionData.getY();
if (min > y) min = y;
if (max < y) max = y;
}
// load sections into ordered array
this.sections = new Section[1 + max - min];
for (SectionData sectionData : sectionsData) {
Section section = new Section(sectionData);
int y = section.getSectionY();
if (min > y) min = y;
if (max < y) max = y;
this.sections[section.sectionY - min] = section;
}
this.sectionMin = min;
this.sectionMax = max;
} else {
this.sections = new Section[0];
this.sectionMin = 0;
this.sectionMax = 0;
}
}
@Override
public boolean isGenerated() {
return generated;
}
@Override
public boolean hasLightData() {
return hasLightData;
}
@Override
public long getInhabitedTime() {
return inhabitedTime;
}
@Override
public BlockState getBlockState(int x, int y, int z) {
Section section = getSection(y >> 4);
if (section == null) return BlockState.AIR;
return section.getBlockState(x, y, z);
}
@Override
public String getBiome(int x, int y, int z) {
Section section = getSection(y >> 4);
if (section == null) return Biome.DEFAULT.getFormatted();
return section.getBiome(x, y, z);
}
@Override
public LightData getLightData(int x, int y, int z, LightData target) {
if (!hasLightData) return target.set(skyLight, 0);
int sectionY = y >> 4;
Section section = getSection(sectionY);
if (section == null) return (sectionY < sectionMin) ? target.set(0, 0) : target.set(skyLight, 0);
return section.getLightData(x, y, z, target);
}
@Override
public int getMinY(int x, int z) {
return sectionMin * 16;
}
@Override
public int getMaxY(int x, int z) {
return sectionMax * 16 + 15;
}
@Override
public boolean hasWorldSurfaceHeights() {
return hasWorldSurfaceHeights;
}
@Override
public int getWorldSurfaceY(int x, int z) {
return worldSurfaceHeights.get((z & 0xF) << 4 | x & 0xF) + worldMinY;
}
@Override
public boolean hasOceanFloorHeights() {
return hasOceanFloorHeights;
}
@Override
public int getOceanFloorY(int x, int z) {
return oceanFloorHeights.get((z & 0xF) << 4 | x & 0xF) + worldMinY;
}
private @Nullable Section getSection(int y) {
y -= sectionMin;
if (y < 0 || y >= this.sections.length) return null;
return this.sections[y];
}
protected static class Section {
private final int sectionY;
private final BlockState[] blockPalette;
private final String[] biomePalette;
private final PackedIntArrayAccess blocks;
private final PackedIntArrayAccess biomes;
private final byte[] blockLight;
private final byte[] skyLight;
public Section(SectionData sectionData) {
this.sectionY = sectionData.y;
this.blockPalette = sectionData.blockStates.palette;
this.biomePalette = sectionData.biomes.palette;
this.blocks = new PackedIntArrayAccess(sectionData.blockStates.data, BLOCKS_PER_SECTION);
this.biomes = new PackedIntArrayAccess(sectionData.biomes.data, BIOMES_PER_SECTION);
this.blockLight = sectionData.blockLight;
this.skyLight = sectionData.skyLight;
}
public BlockState getBlockState(int x, int y, int z) {
if (blockPalette.length == 1) return blockPalette[0];
if (blockPalette.length == 0) return BlockState.AIR;
int id = blocks.get((y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF);
if (id >= blockPalette.length) {
Logger.global.noFloodWarning("palette-warning", "Got block-palette id " + id + " but palette has size of " + blockPalette.length + "! (Future occasions of this error will not be logged)");
return BlockState.MISSING;
}
return blockPalette[id];
}
public String getBiome(int x, int y, int z) {
if (biomePalette.length == 1) return biomePalette[0];
if (biomePalette.length == 0) return Biome.DEFAULT.getValue();
int id = biomes.get((y & 0b1100) << 2 | z & 0b1100 | (x & 0b1100) >> 2);
if (id >= biomePalette.length) {
Logger.global.noFloodWarning("biome-palette-warning", "Got biome-palette id " + id + " but palette has size of " + biomePalette.length + "! (Future occasions of this error will not be logged)");
return Biome.DEFAULT.getValue();
}
return biomePalette[id];
}
public LightData getLightData(int x, int y, int z, LightData target) {
if (blockLight.length == 0 && skyLight.length == 0) return target.set(0, 0);
int blockByteIndex = (y & 0xF) << 8 | (z & 0xF) << 4 | x & 0xF;
int blockHalfByteIndex = blockByteIndex >> 1; // blockByteIndex / 2
boolean largeHalf = (blockByteIndex & 0x1) != 0; // (blockByteIndex % 2) == 0
return target.set(
this.skyLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.skyLight[blockHalfByteIndex], largeHalf) : 0,
this.blockLight.length > blockHalfByteIndex ? MCAUtil.getByteHalf(this.blockLight[blockHalfByteIndex], largeHalf) : 0
);
}
public int getSectionY() {
return sectionY;
}
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
public static class Data extends MCAChunk.Data {
private Key status = STATUS_EMPTY;
private long inhabitedTime = 0;
private HeightmapsData heightmaps = EMPTY_HEIGHTMAPS_DATA;
private SectionData @Nullable [] sections = null;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class HeightmapsData {
@NBTName("WORLD_SURFACE") private long[] worldSurface = EMPTY_LONG_ARRAY;
@NBTName("OCEAN_FLOOR") private long[] oceanFloor = EMPTY_LONG_ARRAY;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class SectionData {
private int y = 0;
private byte[] blockLight = EMPTY_BYTE_ARRAY;
private byte[] skyLight = EMPTY_BYTE_ARRAY;
@NBTName("block_states") private BlockStatesData blockStates = EMPTY_BLOCKSTATESDATA;
private BiomesData biomes = EMPTY_BIOMESDATA;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class BlockStatesData {
private BlockState[] palette = EMPTY_BLOCKSTATE_ARRAY;
private long[] data = EMPTY_LONG_ARRAY;
}
@Getter
@SuppressWarnings("FieldMayBeFinal")
protected static class BiomesData {
private String[] palette = EMPTY_STRING_ARRAY;
private long[] data = EMPTY_LONG_ARRAY;
}
}

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.mca;
package de.bluecolored.bluemap.core.world.mca.chunk;
import java.util.Arrays;

View File

@ -0,0 +1,37 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public abstract class MCAChunk implements Chunk {
protected static final int BLOCKS_PER_SECTION = 16 * 16 * 16;
protected static final int BIOMES_PER_SECTION = 4 * 4 * 4;
protected static final int VALUES_PER_HEIGHTMAP = 16 * 16;
protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
protected static final int[] EMPTY_INT_ARRAY = new int[0];
protected static final long[] EMPTY_LONG_ARRAY = new long[0];
protected static final String[] EMPTY_STRING_ARRAY = new String[0];
protected static final BlockState[] EMPTY_BLOCKSTATE_ARRAY = new BlockState[0];
private final MCARegion region;
private final int dataVersion;
public MCAChunk(MCARegion region, Data chunkData) {
this.region = region;
this.dataVersion = chunkData.getDataVersion();
}
@SuppressWarnings("FieldMayBeFinal")
@Getter
public static class Data {
private int dataVersion = 0;
}
}

View File

@ -0,0 +1,40 @@
package de.bluecolored.bluemap.core.world.mca.data;
import de.bluecolored.bluemap.core.world.BlockState;
import de.bluecolored.bluenbt.NBTReader;
import de.bluecolored.bluenbt.TypeDeserializer;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
public class BlockStateDeserializer implements TypeDeserializer<BlockState> {
@Override
public BlockState read(NBTReader reader) throws IOException {
reader.beginCompound();
String id = null;
Map<String, String> properties = null;
while (reader.hasNext()) {
switch (reader.name()) {
case "Name" : id = reader.nextString(); break;
case "Properties" :
properties = new LinkedHashMap<>();
reader.beginCompound();
while (reader.hasNext())
properties.put(reader.name(), reader.nextString());
reader.endCompound();
break;
default : reader.skip();
}
}
reader.endCompound();
if (id == null) throw new IOException("Invalid BlockState, Name is missing!");
return properties == null ? new BlockState(id) : new BlockState(id, properties);
}
}

View File

@ -0,0 +1,16 @@
package de.bluecolored.bluemap.core.world.mca.data;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluenbt.NBTReader;
import de.bluecolored.bluenbt.TypeDeserializer;
import java.io.IOException;
public class KeyDeserializer implements TypeDeserializer<Key> {
@Override
public Key read(NBTReader reader) throws IOException {
return new Key(reader.nextString());
}
}

View File

@ -0,0 +1,40 @@
package de.bluecolored.bluemap.core.world.mca.data;
import de.bluecolored.bluemap.api.debug.DebugDump;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
@Getter
@SuppressWarnings("FieldMayBeFinal")
@DebugDump
public class LevelData {
private Data data = new Data();
@Getter
@DebugDump
public static class Data {
private String levelName = "world";
private int spawnX = 0, spawnY = 0, spawnZ = 0;
private WGSettings worldGenSettings = new WGSettings();
}
@Getter
@DebugDump
public static class WGSettings {
private Map<String, Dimension> dimensions = new HashMap<>();
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@DebugDump
public static class Dimension {
private String type = "minecraft:overworld";
}
}

View File

@ -0,0 +1,197 @@
/*
* 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.world.mca.region;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunk;
import lombok.Getter;
import lombok.ToString;
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@Getter
@ToString
public class MCARegion implements Region {
public static final String FILE_SUFFIX = ".mca";
private final MCAWorld world;
private final Path regionFile;
private final Vector2i regionPos;
public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentException {
this.world = world;
this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\.");
int rX = Integer.parseInt(filenameParts[1]);
int rZ = Integer.parseInt(filenameParts[2]);
this.regionPos = new Vector2i(rX, rZ);
}
public MCARegion(MCAWorld world, Vector2i regionPos) throws IllegalArgumentException {
this.world = world;
this.regionPos = regionPos;
this.regionFile = world.getRegionFolder().resolve(getRegionFileName(regionPos.getX(), regionPos.getY()));
}
@Override
public Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
if (Files.notExists(regionFile)) return Chunk.EMPTY_CHUNK;
long fileLength = Files.size(regionFile);
if (fileLength == 0) return Chunk.EMPTY_CHUNK;
try (FileChannel channel = FileChannel.open(regionFile, StandardOpenOption.READ)) {
int xzChunk = (chunkZ & 0b11111) << 5 | (chunkX & 0b11111);
byte[] header = new byte[4];
channel.position(xzChunk * 4);
readFully(channel, header, 0, 4);
int offset = header[0] << 16;
offset |= (header[1] & 0xFF) << 8;
offset |= header[2] & 0xFF;
offset *= 4096;
int size = header[3] * 4096;
if (size == 0) return Chunk.EMPTY_CHUNK;
byte[] chunkDataBuffer = new byte[size];
channel.position(offset);
readFully(channel, chunkDataBuffer, 0, size);
return loadChunk(chunkDataBuffer, size);
}
}
@Override
public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
if (Files.notExists(regionFile)) return;
long fileLength = Files.size(regionFile);
if (fileLength == 0) return;
int chunkStartX = regionPos.getX() * 32;
int chunkStartZ = regionPos.getY() * 32;
try (FileChannel channel = FileChannel.open(regionFile, StandardOpenOption.READ)) {
byte[] header = new byte[1024 * 8];
byte[] chunkDataBuffer = null;
// read the header
readFully(channel, header, 0, header.length);
// iterate over all chunks
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
int xzChunk = z * 32 + x;
int size = header[xzChunk * 4 + 3] * 4096;
if (size == 0) continue;
int chunkX = chunkStartX + x;
int chunkZ = chunkStartZ + z;
int i = xzChunk * 4 + 4096;
int timestamp = header[i++] << 24;
timestamp |= (header[i++] & 0xFF) << 16;
timestamp |= (header[i++] & 0xFF) << 8;
timestamp |= header[i] & 0xFF;
// load chunk only if consumers filter returns true
if (consumer.filter(chunkX, chunkZ, timestamp)) {
i = xzChunk * 4;
int offset = header[i++] << 16;
offset |= (header[i++] & 0xFF) << 8;
offset |= header[i] & 0xFF;
offset *= 4096;
if (chunkDataBuffer == null || chunkDataBuffer.length < size)
chunkDataBuffer = new byte[size];
channel.position(offset);
readFully(channel, chunkDataBuffer, 0, size);
MCAChunk chunk = loadChunk(chunkDataBuffer, size);
consumer.accept(chunkX, chunkZ, chunk);
}
}
}
}
}
private MCAChunk loadChunk(byte[] data, int size) throws IOException {
int compressionTypeId = data[4];
Compression compression;
switch (compressionTypeId) {
case 0 :
case 3 : compression = Compression.NONE; break;
case 1 : compression = Compression.GZIP; break;
case 2 : compression = Compression.DEFLATE; break;
case 4 : compression = Compression.LZ4; break;
default: throw new IOException("Unknown chunk compression-id: " + compressionTypeId);
}
return world.getChunkLoader().load(this, data, 5, size - 5, compression);
}
public static String getRegionFileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX;
}
@SuppressWarnings("SameParameterValue")
private static void readFully(ReadableByteChannel src, byte[] dst, int off, int len) throws IOException {
readFully(src, ByteBuffer.wrap(dst), off, len);
}
private static void readFully(ReadableByteChannel src, ByteBuffer bb, int off, int len) throws IOException {
int limit = off + len;
if (limit > bb.capacity()) throw new IllegalArgumentException("buffer too small");
bb.limit(limit);
bb.position(off);
do {
int read = src.read(bb);
if (read < 0) throw new EOFException();
} while (bb.remaining() > 0);
}
}

Some files were not shown because too many files have changed in this diff Show More