mirror of
https://github.com/BlueMap-Minecraft/BlueMap.git
synced 2024-11-22 10:35:16 +01:00
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:
parent
efd45658d5
commit
16981f2797
@ -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")
|
||||
|
@ -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();
|
||||
|
||||
}
|
@ -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)
|
||||
this.resourcePack = preloadedResourcePack;
|
||||
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,27 +314,33 @@ 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 defaultResourceFile = config.getCoreConfig().getData().resolve("minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
|
||||
Path resourceExtensionsFile = config.getCoreConfig().getData().resolve("resourceExtensions.zip");
|
||||
|
||||
Path resourcePackFolder = serverInterface.getConfigFolder().resolve("resourcepacks");
|
||||
try {
|
||||
FileHelper.createDirectories(resourcePackFolder);
|
||||
} catch (IOException ex) {
|
||||
throw new ConfigurationException(
|
||||
"BlueMap failed to create this folder:\n" +
|
||||
resourcePackFolder + "\n" +
|
||||
"Does BlueMap have sufficient permissions?",
|
||||
ex);
|
||||
}
|
||||
|
||||
try {
|
||||
FileHelper.createDirectories(resourcePackFolder);
|
||||
} catch (IOException ex) {
|
||||
throw new ConfigurationException(
|
||||
"BlueMap failed to create this folder:\n" +
|
||||
resourcePackFolder + "\n" +
|
||||
"Does BlueMap have sufficient permissions?",
|
||||
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,37 +359,43 @@ public synchronized ResourcePack getResourcePack() throws ConfigurationException
|
||||
}
|
||||
}
|
||||
|
||||
if (Thread.interrupted()) throw new InterruptedException();
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(resourceExtensionsFile);
|
||||
FileHelper.createDirectories(resourceExtensionsFile.getParent());
|
||||
URL resourceExtensionsUrl = Objects.requireNonNull(
|
||||
Plugin.class.getResource(
|
||||
"/de/bluecolored/bluemap/" + minecraftVersion.getResource().getResourcePrefix() +
|
||||
"/resourceExtensions.zip")
|
||||
"/resourceExtensions.zip")
|
||||
);
|
||||
FileUtils.copyURLToFile(resourceExtensionsUrl, resourceExtensionsFile.toFile(), 10000, 10000);
|
||||
} catch (IOException ex) {
|
||||
throw new ConfigurationException(
|
||||
"Failed to create resourceExtensions.zip!\n" +
|
||||
"Does BlueMap has sufficient write permissions?",
|
||||
"Does BlueMap has sufficient write permissions?",
|
||||
ex);
|
||||
}
|
||||
|
||||
if (Thread.interrupted()) throw new InterruptedException();
|
||||
|
||||
try {
|
||||
resourcePack = new ResourcePack();
|
||||
ResourcePack resourcePack = new ResourcePack();
|
||||
|
||||
List<Path> resourcePackRoots = new ArrayList<>();
|
||||
// load from resourcepack folder
|
||||
try (Stream<Path> resourcepackFiles = Files.list(resourcePackFolder)) {
|
||||
resourcepackFiles
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(resourcePackRoots::add);
|
||||
|
||||
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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
World coreWorld = plugin.getWorld(serverWorld);
|
||||
if (coreWorld == null) return Optional.empty();
|
||||
|
||||
return Optional.of(new BlueMapWorldImpl(plugin, coreWorld));
|
||||
}
|
||||
|
||||
@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();
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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");
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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,38 +510,41 @@ public synchronized void save() {
|
||||
}
|
||||
}
|
||||
|
||||
if (maps != null) {
|
||||
for (BmMap map : maps.values()) {
|
||||
map.save();
|
||||
}
|
||||
var maps = blueMap.getMaps();
|
||||
for (BmMap map : maps.values()) {
|
||||
map.save();
|
||||
}
|
||||
}
|
||||
|
||||
public void saveMarkerStates() {
|
||||
if (maps != null) {
|
||||
for (BmMap map : maps.values()) {
|
||||
map.saveMarkerState();
|
||||
}
|
||||
if (blueMap == null) return;
|
||||
|
||||
var maps = blueMap.getMaps();
|
||||
for (BmMap map : maps.values()) {
|
||||
map.saveMarkerState();
|
||||
}
|
||||
}
|
||||
|
||||
public void savePlayerStates() {
|
||||
if (maps != null) {
|
||||
for (BmMap map : maps.values()) {
|
||||
var dataSupplier = new LivePlayersDataSupplier(
|
||||
serverInterface,
|
||||
getConfigs().getPluginConfig(),
|
||||
map.getWorldId(),
|
||||
Predicate.not(pluginState::isPlayerHidden)
|
||||
);
|
||||
try (
|
||||
OutputStream out = map.getStorage().writeMeta(map.getId(), BmMap.META_FILE_PLAYERS);
|
||||
Writer writer = new OutputStreamWriter(out)
|
||||
) {
|
||||
writer.write(dataSupplier.get());
|
||||
} catch (Exception ex) {
|
||||
Logger.global.logError("Failed to save players for map '" + map.getId() + "'!", ex);
|
||||
}
|
||||
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,
|
||||
getBlueMap().getConfig().getPluginConfig(),
|
||||
serverWorld,
|
||||
Predicate.not(pluginState::isPlayerHidden)
|
||||
);
|
||||
try (
|
||||
OutputStream out = map.getStorage().writeMeta(map.getId(), BmMap.META_FILE_PLAYERS);
|
||||
Writer writer = new OutputStreamWriter(out)
|
||||
) {
|
||||
writer.write(dataSupplier.get());
|
||||
} catch (Exception ex) {
|
||||
Logger.global.logError("Failed to save players for map '" + map.getId() + "'!", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,9 +653,12 @@ public PlayerSkinUpdater getSkinUpdater() {
|
||||
}
|
||||
|
||||
private void initFileWatcherTasks() {
|
||||
for (BmMap map : maps.values()) {
|
||||
if (pluginState.getMapState(map).isUpdateEnabled()) {
|
||||
startWatchingMap(map);
|
||||
var maps = blueMap.getMaps();
|
||||
if (maps != null) {
|
||||
for (BmMap map : maps.values()) {
|
||||
if (pluginState.getMapState(map).isUpdateEnabled()) {
|
||||
startWatchingMap(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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/]+).*");
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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++) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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() +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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(), "/");
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
|
@ -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 -> {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package de.bluecolored.bluemap.core.util;
|
||||
|
||||
public interface Keyed {
|
||||
|
||||
Key getKey();
|
||||
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
|
@ -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; }
|
||||
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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; }
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
value = value & 0xF;
|
||||
return value;
|
||||
if (largeHalf) return value >> 4 & 0xF;
|
||||
return value & 0xF;
|
||||
}
|
||||
|
||||
public static int ceilLog2(int n) {
|
||||
return Integer.SIZE - Integer.numberOfLeadingZeros(n - 1);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user