Renderstate rewrite, and moving biomes to datapacks, mc-version and vanilla-resources and resource-extensions rewrite (wip)

This commit is contained in:
Lukas Rieger (Blue) 2024-05-07 16:45:24 +02:00
parent a6402850c9
commit 81fe41fd2b
No known key found for this signature in database
GPG Key ID: AA33883B1BBA03E6
131 changed files with 3918 additions and 2820 deletions

View File

@ -26,7 +26,6 @@
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;
@ -34,7 +33,7 @@
public interface BlueMapConfiguration {
MinecraftVersion getMinecraftVersion();
@Nullable String getMinecraftVersion();
CoreConfig getCoreConfig();
@ -48,7 +47,7 @@ public interface BlueMapConfiguration {
Map<String, StorageConfig> getStorageConfigs();
@Nullable Path getResourcePacksFolder();
@Nullable Path getPacksFolder();
@Nullable Path getModsFolder();

View File

@ -36,12 +36,13 @@
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.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.resources.datapack.DataPack;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.MinecraftVersion;
import de.bluecolored.bluemap.core.resources.VersionManifest;
import de.bluecolored.bluemap.core.resources.pack.datapack.DataPack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Key;
@ -74,6 +75,7 @@ public class BlueMapService implements Closeable {
private final BlueMapConfiguration config;
private final WebFilesManager webFilesManager;
private MinecraftVersion minecraftVersion;
private ResourcePack resourcePack;
private final Map<String, World> worlds;
private final Map<String, BmMap> maps;
@ -225,7 +227,7 @@ private synchronized void loadMap(String id, MapConfig mapConfig) throws Configu
if (world == null) {
try {
Logger.global.logDebug("Loading world " + worldId + " ...");
world = MCAWorld.load(worldFolder, dimension);
world = MCAWorld.load(worldFolder, dimension, loadDataPack(worldFolder));
worlds.put(worldId, world);
} catch (IOException ex) {
throw new ConfigurationException(
@ -320,108 +322,17 @@ public synchronized Storage getOrLoadStorage(String storageId) throws Configurat
public synchronized ResourcePack getOrLoadResourcePack() throws ConfigurationException, InterruptedException {
if (resourcePack == null) {
MinecraftVersion minecraftVersion = config.getMinecraftVersion();
@Nullable Path resourcePackFolder = config.getResourcePacksFolder();
@Nullable Path modsFolder = config.getModsFolder();
Path defaultResourceFile = config.getCoreConfig().getData().resolve("minecraft-client-" + minecraftVersion.getResource().getVersion().getVersionString() + ".jar");
Path resourceExtensionsFile = config.getCoreConfig().getData().resolve("resourceExtensions.zip");
try {
FileHelper.createDirectories(resourcePackFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap failed to create this folder:\n" +
resourcePackFolder + "\n" +
"Does BlueMap have sufficient permissions?",
ex);
}
MinecraftVersion minecraftVersion = getOrLoadMinecraftVersion();
Path vanillaResourcePack = minecraftVersion.getResourcePack();
if (Thread.interrupted()) throw new InterruptedException();
if (!Files.exists(defaultResourceFile)) {
if (config.getCoreConfig().isAcceptDownload()) {
//download file
try {
Logger.global.logInfo("Downloading " + minecraftVersion.getResource().getClientUrl() + " to " + defaultResourceFile + " ...");
FileHelper.createDirectories(defaultResourceFile.getParent());
Path tempResourceFile = defaultResourceFile.getParent().resolve(defaultResourceFile.getFileName() + ".filepart");
Files.deleteIfExists(tempResourceFile);
FileUtils.copyURLToFile(new URL(minecraftVersion.getResource().getClientUrl()), tempResourceFile.toFile(), 10000, 10000);
FileHelper.move(tempResourceFile, defaultResourceFile);
} catch (IOException ex) {
throw new ConfigurationException("Failed to download resources!", ex);
}
} else {
throw new MissingResourcesException();
}
}
if (Thread.interrupted()) throw new InterruptedException();
Deque<Path> packRoots = getPackRoots();
packRoots.addLast(vanillaResourcePack);
try {
Files.deleteIfExists(resourceExtensionsFile);
FileHelper.createDirectories(resourceExtensionsFile.getParent());
URL resourceExtensionsUrl = Objects.requireNonNull(
Plugin.class.getResource(
"/de/bluecolored/bluemap/" + minecraftVersion.getResource().getResourcePrefix() +
"/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?",
ex);
}
if (Thread.interrupted()) throw new InterruptedException();
try {
ResourcePack resourcePack = new ResourcePack();
List<Path> resourcePackRoots = new ArrayList<>();
if (resourcePackFolder != null) {
// load from resourcepack folder
try (Stream<Path> resourcepackFiles = Files.list(resourcePackFolder)) {
resourcepackFiles
.sorted(Comparator.reverseOrder())
.forEach(resourcePackRoots::add);
}
}
if (config.getCoreConfig().isScanForModResources()) {
// load from mods folder
if (modsFolder != null && Files.isDirectory(modsFolder)) {
try (Stream<Path> resourcepackFiles = Files.list(modsFolder)) {
resourcepackFiles
.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".jar"))
.forEach(resourcePackRoots::add);
}
}
// load from datapacks
for (Path worldFolder : getWorldFolders()) {
Path datapacksFolder = worldFolder.resolve("datapacks");
if (!Files.isDirectory(datapacksFolder)) continue;
try (Stream<Path> resourcepackFiles = Files.list(worldFolder.resolve("datapacks"))) {
resourcepackFiles.forEach(resourcePackRoots::add);
}
}
}
resourcePackRoots.add(resourceExtensionsFile);
resourcePackRoots.add(defaultResourceFile);
resourcePack.loadResources(resourcePackRoots);
ResourcePack resourcePack = new ResourcePack(minecraftVersion.getResourcePackVersion());
resourcePack.loadResources(packRoots);
this.resourcePack = resourcePack;
} catch (IOException | RuntimeException e) {
throw new ConfigurationException("Failed to parse resources!\n" +
@ -432,17 +343,125 @@ public synchronized ResourcePack getOrLoadResourcePack() throws ConfigurationExc
return this.resourcePack;
}
private Collection<Path> getWorldFolders() {
Set<Path> folders = new HashSet<>();
for (MapConfig mapConfig : config.getMapConfigs().values()) {
Path folder = mapConfig.getWorld();
if (folder == null) continue;
folder = folder.toAbsolutePath().normalize();
if (Files.isDirectory(folder)) {
folders.add(folder);
public synchronized DataPack loadDataPack(Path worldFolder) throws ConfigurationException, InterruptedException {
MinecraftVersion minecraftVersion = getOrLoadMinecraftVersion();
Path vanillaDataPack = minecraftVersion.getDataPack();
if (Thread.interrupted()) throw new InterruptedException();
// also load world datapacks
Iterable<Path> worldPacks = List.of();
Path worldPacksFolder = worldFolder.resolve("datapacks");
if (Files.isDirectory(worldPacksFolder)) {
try (Stream<Path> worldPacksStream = Files.list(worldPacksFolder)) {
worldPacks = worldPacksStream.toList();
} catch (IOException e) {
throw new ConfigurationException("Failed to access the worlds datapacks folder.", e);
}
}
return folders;
Deque<Path> packRoots = getPackRoots(worldPacks);
packRoots.addLast(vanillaDataPack);
try {
DataPack datapack = new DataPack(minecraftVersion.getDataPackVersion());
datapack.loadResources(packRoots);
return datapack;
} catch (IOException | RuntimeException e) {
throw new ConfigurationException("Failed to parse resources!\n" +
"Is one of your resource-packs corrupted?", e);
}
}
private synchronized Deque<Path> getPackRoots(Path... additionalRoots) throws ConfigurationException, InterruptedException {
return getPackRoots(List.of(additionalRoots));
}
private synchronized Deque<Path> getPackRoots(Iterable<Path> additionalRoots) throws ConfigurationException, InterruptedException {
@Nullable Path packsFolder = config.getPacksFolder();
@Nullable Path modsFolder = config.getModsFolder();
try {
FileHelper.createDirectories(packsFolder);
} catch (IOException ex) {
throw new ConfigurationException(
"BlueMap failed to create this folder:\n" +
packsFolder + "\n" +
"Does BlueMap have sufficient permissions?",
ex);
}
Path resourceExtensionsFile = config.getCoreConfig().getData().resolve("resourceExtensions.zip");
if (Thread.interrupted()) throw new InterruptedException();
try {
Files.deleteIfExists(resourceExtensionsFile);
FileHelper.createDirectories(resourceExtensionsFile.getParent());
URL resourceExtensionsUrl = Objects.requireNonNull(
Plugin.class.getResource("/de/bluecolored/bluemap/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?",
ex);
}
Deque<Path> packRoots = new LinkedList<>();
// load from pack folder
if (packsFolder != null && Files.isDirectory(packsFolder)) {
try (Stream<Path> packFiles = Files.list(packsFolder)) {
packFiles
.sorted(Comparator.reverseOrder())
.forEach(packRoots::add);
} catch (IOException e) {
throw new ConfigurationException("Failed to access packs folder.", e);
}
}
// add additional roots
additionalRoots.forEach(packRoots::add);
// load from mods folder
if (config.getCoreConfig().isScanForModResources() && modsFolder != null && Files.isDirectory(modsFolder)) {
try (Stream<Path> packFiles = Files.list(modsFolder)) {
packFiles
.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".jar"))
.forEach(packRoots::add);
} catch (IOException e) {
throw new ConfigurationException("Failed to access packs folder.", e);
}
}
packRoots.add(resourceExtensionsFile);
return packRoots;
}
public synchronized MinecraftVersion getOrLoadMinecraftVersion() throws ConfigurationException {
if (this.minecraftVersion == null) {
try {
this.minecraftVersion = MinecraftVersion.load(
config.getMinecraftVersion(),
config.getCoreConfig().getData(),
config.getCoreConfig().isAcceptDownload()
);
} catch (IOException ex) {
if (!config.getCoreConfig().isAcceptDownload()) {
throw new MissingResourcesException();
} else {
throw new ConfigurationException("""
BlueMap was not able to download some important resources!
Make sure BlueMap is able to connect to mojang-servers (%s)."""
.formatted(VersionManifest.DOMAIN), ex);
}
}
}
return this.minecraftVersion;
}
public BlueMapConfiguration getConfig() {

View File

@ -49,13 +49,13 @@ public RenderManagerImpl(BlueMapAPIImpl api, Plugin plugin) {
@Override
public boolean scheduleMapUpdateTask(BlueMapMap map, boolean force) {
BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), force));
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), s -> force));
}
@Override
public boolean scheduleMapUpdateTask(BlueMapMap map, Collection<Vector2i> regions, boolean force) {
BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), regions, force));
return renderManager.scheduleRenderTask(new MapUpdateTask(cmap.getBmMap(), regions, s -> force));
}
@Override

View File

@ -29,9 +29,8 @@
import de.bluecolored.bluemap.common.config.storage.StorageConfig;
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.resources.pack.datapack.DataPack;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Key;
import lombok.Builder;
@ -51,26 +50,26 @@ public class BlueMapConfigManager implements BlueMapConfiguration {
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 Path packsFolder;
private final @Nullable String minecraftVersion;
private final @Nullable Path modsFolder;
@Builder
private BlueMapConfigManager(
@NonNull MinecraftVersion minecraftVersion,
@NonNull Path configRoot,
@Nullable String minecraftVersion,
@Nullable Path defaultDataFolder,
@Nullable Path defaultWebroot,
@Nullable Collection<ServerWorld> autoConfigWorlds,
@Nullable Boolean usePluginConfig,
@Nullable Boolean useMetricsConfig,
@Nullable Path resourcePacksFolder,
@Nullable Path packsFolder,
@Nullable Path modsFolder
) throws ConfigurationException {
// set defaults
@ -79,10 +78,9 @@ private BlueMapConfigManager(
if (autoConfigWorlds == null) autoConfigWorlds = Collections.emptyList();
if (usePluginConfig == null) usePluginConfig = true;
if (useMetricsConfig == null) useMetricsConfig = true;
if (resourcePacksFolder == null) resourcePacksFolder = configRoot.resolve("resourcepacks");
if (packsFolder == null) packsFolder = configRoot.resolve("packs");
// load
this.minecraftVersion = minecraftVersion;
this.configManager = new ConfigManager(configRoot);
this.coreConfig = loadCoreConfig(defaultDataFolder, useMetricsConfig);
this.webappConfig = loadWebappConfig(defaultWebroot);
@ -90,7 +88,8 @@ private BlueMapConfigManager(
this.pluginConfig = usePluginConfig ? loadPluginConfig() : new PluginConfig();
this.storageConfigs = Collections.unmodifiableMap(loadStorageConfigs(webappConfig.getWebroot()));
this.mapConfigs = Collections.unmodifiableMap(loadMapConfigs(autoConfigWorlds));
this.resourcePacksFolder = resourcePacksFolder;
this.packsFolder = packsFolder;
this.minecraftVersion = minecraftVersion;
this.modsFolder = modsFolder;
}

View File

@ -39,10 +39,10 @@
public interface Dialect extends Keyed {
Dialect MYSQL = new Impl(Key.bluemap("mysql"), MySQLCommandSet::new);
Dialect MARIADB = new Impl(Key.bluemap("mariadb"), MySQLCommandSet::new);
Dialect POSTGRESQL = new Impl(Key.bluemap("postgresql"), PostgreSQLCommandSet::new);
Dialect SQLITE = new Impl(Key.bluemap("sqlite"), SqliteCommandSet::new);
Dialect MYSQL = new Impl(Key.bluemap("mysql"), "jdbc:mysql:", MySQLCommandSet::new);
Dialect MARIADB = new Impl(Key.bluemap("mariadb"), "jdbc:mariadb:", MySQLCommandSet::new);
Dialect POSTGRESQL = new Impl(Key.bluemap("postgresql"), "jdbc:postgresql:", PostgreSQLCommandSet::new);
Dialect SQLITE = new Impl(Key.bluemap("sqlite"), "jdbc:sqlite:", SqliteCommandSet::new);
Registry<Dialect> REGISTRY = new Registry<>(
MYSQL,
@ -51,16 +51,23 @@ public interface Dialect extends Keyed {
SQLITE
);
boolean supports(String connectionUrl);
CommandSet createCommandSet(Database database);
@RequiredArgsConstructor
class Impl implements Dialect {
@Getter
private final Key key;
@Getter private final Key key;
private final String protocol;
private final Function<Database, CommandSet> commandSetProvider;
@Override
public boolean supports(String connectionUrl) {
return connectionUrl.startsWith(protocol);
}
@Override
public CommandSet createCommandSet(Database database) {
return commandSetProvider.apply(database);

View File

@ -41,6 +41,7 @@ public class FileConfig extends StorageConfig {
private Path root = Path.of("bluemap", "web", "maps");
private String compression = Compression.GZIP.getKey().getFormatted();
private boolean atomic = true;
public Compression getCompression() throws ConfigurationException {
return parseKey(Compression.REGISTRY, compression, "compression");
@ -48,7 +49,7 @@ public Compression getCompression() throws ConfigurationException {
@Override
public FileStorage createStorage() throws ConfigurationException {
return new FileStorage(root, getCompression());
return new FileStorage(root, getCompression(), atomic);
}
}

View File

@ -30,7 +30,6 @@
import de.bluecolored.bluemap.core.storage.sql.Database;
import de.bluecolored.bluemap.core.storage.sql.SQLStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.Key;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
@ -46,16 +45,12 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
@ConfigSerializable
@Getter
public class SQLConfig extends StorageConfig {
private static final Pattern URL_DIALECT_PATTERN = Pattern.compile("jdbc:([^:]*):.*");
private String connectionUrl = "jdbc:mysql://localhost/bluemap?permitMysqlScheme";
private Map<String, String> connectionProperties = new HashMap<>();
@DebugDump private String dialect = null;
@ -101,15 +96,18 @@ public Dialect getDialect() throws ConfigurationException {
// default from connection-url
if (key == null) {
Matcher matcher = URL_DIALECT_PATTERN.matcher(connectionUrl);
if (!matcher.find()) {
throw new ConfigurationException("""
Failed to parse the provided connection-url!
for (Dialect d : Dialect.REGISTRY.values()) {
if (d.supports(connectionUrl)) {
key = d.getKey().getFormatted();
break;
}
}
if (key == null) throw new ConfigurationException("""
Could not find any sql-dialect that is matching the given connection-url.
Please check your 'connection-url' setting in your configuration and make sure it is in the correct format.
""".strip());
}
key = Key.bluemap(matcher.group(1)).getFormatted();
}
return parseKey(Dialect.REGISTRY, key, "dialect");
}

View File

@ -44,7 +44,8 @@
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.metrics.Metrics;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.MinecraftVersion;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileHelper;
import de.bluecolored.bluemap.core.util.Tristate;
@ -124,7 +125,7 @@ private void load(@Nullable ResourcePack preloadedResourcePack) throws IOExcepti
BlueMapConfigManager configManager = BlueMapConfigManager.builder()
.minecraftVersion(serverInterface.getMinecraftVersion())
.configRoot(serverInterface.getConfigFolder())
.resourcePacksFolder(serverInterface.getConfigFolder().resolve("resourcepacks"))
.packsFolder(serverInterface.getConfigFolder().resolve("packs"))
.modsFolder(serverInterface.getModsFolder().orElse(null))
.useMetricsConfig(serverInterface.isMetricsEnabled() == Tristate.UNDEFINED)
.autoConfigWorlds(serverInterface.getLoadedServerWorlds())
@ -287,7 +288,7 @@ public void run() {
save();
}
};
daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(2), TimeUnit.MINUTES.toMillis(2));
daemonTimer.schedule(saveTask, TimeUnit.MINUTES.toMillis(10), TimeUnit.MINUTES.toMillis(10));
//periodically save markers
int writeMarkersInterval = pluginConfig.getWriteMarkersInterval();
@ -341,11 +342,12 @@ public void run() {
}
//metrics
MinecraftVersion minecraftVersion = blueMap.getOrLoadMinecraftVersion();
TimerTask metricsTask = new TimerTask() {
@Override
public void run() {
if (serverInterface.isMetricsEnabled().getOr(coreConfig::isMetrics))
Metrics.sendReport(implementationType, configManager.getMinecraftVersion().getVersionString());
Metrics.sendReport(implementationType, minecraftVersion.getId());
}
};
daemonTimer.scheduleAtFixedRate(metricsTask, TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(30));
@ -389,12 +391,11 @@ public void run() {
public void unload() {
this.unload(false);
}
public void unload(boolean keepWebserver) {
loadingLock.interruptAndLock();
try {
synchronized (this) {
//save
save();
//disable api
if (api != null) api.unregister();
@ -415,8 +416,18 @@ public void unload(boolean keepWebserver) {
}
regionFileWatchServices = null;
//stop services
// stop render-manager
if (renderManager != null){
if (renderManager.getCurrentRenderTask() != null) {
renderManager.removeAllRenderTasks();
if (!renderManager.isRunning()) renderManager.start(1);
try {
renderManager.awaitIdle(true);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
renderManager.stop();
try {
renderManager.awaitShutdown();
@ -424,8 +435,11 @@ public void unload(boolean keepWebserver) {
Thread.currentThread().interrupt();
}
}
renderManager = null;
//save
save();
// stop webserver
if (webServer != null && !keepWebserver) {
try {
webServer.close();
@ -435,7 +449,7 @@ public void unload(boolean keepWebserver) {
webServer = null;
}
if (webLogger != null) {
if (webLogger != null && !keepWebserver) {
try {
webLogger.close();
} catch (Exception ex) {

View File

@ -67,7 +67,11 @@ public RegionFileWatchService(RenderManager renderManager, BmMap map) throws IOE
FileHelper.createDirectories(folder);
this.watchService = folder.getFileSystem().newWatchService();
folder.register(this.watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
folder.register(this.watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
);
Logger.global.logDebug("Created region-file watch-service for map '" + map.getId() + "' at '" + folder + "'.");
}

View File

@ -24,6 +24,7 @@
*/
package de.bluecolored.bluemap.common.plugin.commands;
import com.flowpowered.math.vector.Vector2d;
import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector3i;
@ -47,14 +48,15 @@
import de.bluecolored.bluemap.common.rendermanager.*;
import de.bluecolored.bluemap.common.serverinterface.CommandSource;
import de.bluecolored.bluemap.core.BlueMap;
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.map.MapRenderState;
import de.bluecolored.bluemap.core.map.renderstate.TileInfoRegion;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.World;
import de.bluecolored.bluemap.core.world.block.Block;
import de.bluecolored.bluemap.core.world.block.entity.BlockEntity;
@ -123,6 +125,15 @@ public void init() {
.then(argument("z", DoubleArgumentType.doubleArg())
.executes(this::debugBlockCommand))))))
.then(literal("map")
.requires(requirements("bluemap.debug"))
.then(argument("map", StringArgumentType.string()).suggests(new MapSuggestionProvider<>(plugin))
.executes(this::debugMapCommand)
.then(argument("x", IntegerArgumentType.integer())
.then(argument("z", IntegerArgumentType.integer())
.executes(this::debugMapCommand)))))
.then(literal("flush")
.requires(requirements("bluemap.debug"))
.executes(this::debugFlushCommand)
@ -329,32 +340,27 @@ public int versionCommand(CommandContext<S> context) {
renderThreadCount = plugin.getRenderManager().getWorkerThreadCount();
}
MinecraftVersion minecraftVersion = plugin.getServerInterface().getMinecraftVersion();
String minecraftVersion = plugin.getServerInterface().getMinecraftVersion();
source.sendMessage(Text.of(TextFormat.BOLD, TextColor.BLUE, "Version: ", TextColor.WHITE, BlueMap.VERSION));
source.sendMessage(Text.of(TextColor.GRAY, "Commit: ", TextColor.WHITE, BlueMap.GIT_HASH));
source.sendMessage(Text.of(TextColor.GRAY, "Implementation: ", TextColor.WHITE, plugin.getImplementationType()));
source.sendMessage(Text.of(
TextColor.GRAY, "Minecraft compatibility: ", TextColor.WHITE, minecraftVersion.getVersionString(),
TextColor.GRAY, " (" + minecraftVersion.getResource().getVersion().getVersionString() + ")"
));
source.sendMessage(Text.of(TextColor.GRAY, "Minecraft: ", TextColor.WHITE, minecraftVersion));
source.sendMessage(Text.of(TextColor.GRAY, "Render-threads: ", TextColor.WHITE, renderThreadCount));
source.sendMessage(Text.of(TextColor.GRAY, "Available processors: ", TextColor.WHITE, Runtime.getRuntime().availableProcessors()));
source.sendMessage(Text.of(TextColor.GRAY, "Available memory: ", TextColor.WHITE, (Runtime.getRuntime().maxMemory() / 1024L / 1024L) + " MiB"));
if (minecraftVersion.isAtLeast(new MinecraftVersion(1, 15))) {
String clipboardValue =
"Version: " + BlueMap.VERSION + "\n" +
"Commit: " + BlueMap.GIT_HASH + "\n" +
"Implementation: " + plugin.getImplementationType() + "\n" +
"Minecraft compatibility: " + minecraftVersion.getVersionString() + " (" + minecraftVersion.getResource().getVersion().getVersionString() + ")\n" +
"Minecraft: " + minecraftVersion + "\n" +
"Render-threads: " + renderThreadCount + "\n" +
"Available processors: " + Runtime.getRuntime().availableProcessors() + "\n" +
"Available memory: " + Runtime.getRuntime().maxMemory() / 1024L / 1024L + " MiB";
source.sendMessage(Text.of(TextColor.DARK_GRAY, "[copy to clipboard]")
.setClickAction(Text.ClickAction.COPY_TO_CLIPBOARD, clipboardValue)
.setHoverText(Text.of(TextColor.GRAY, "click to copy the above text .. ", TextFormat.ITALIC, TextColor.GRAY, "duh!")));
}
return 1;
}
@ -467,6 +473,86 @@ public int debugFlushCommand(CommandContext<S> context) {
return 1;
}
public int debugMapCommand(CommandContext<S> context) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
// parse arguments
String mapId = context.getArgument("map", String.class);
Optional<Integer> x = getOptionalArgument(context, "x", Integer.class);
Optional<Integer> z = getOptionalArgument(context, "z", Integer.class);
final BmMap map = parseMap(mapId).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this id: ", TextColor.WHITE, mapId));
return 0;
}
final Vector2i position;
if (x.isPresent() && z.isPresent()) {
position = new Vector2i(x.get(), z.get());
} else {
position = source.getPosition()
.map(v -> v.toVector2(true))
.map(Vector2d::floor)
.map(Vector2d::toInt)
.orElse(null);
if (position == null) {
source.sendMessage(Text.of(TextColor.RED, "Can't detect a location from this command-source, you'll have to define a position!"));
return 0;
}
}
new Thread(() -> {
// collect and output debug info
Grid chunkGrid = map.getWorld().getChunkGrid();
Grid regionGrid = map.getWorld().getRegionGrid();
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Vector2i regionPos = regionGrid.getCell(position);
Vector2i chunkPos = chunkGrid.getCell(position);
Vector2i tilePos = tileGrid.getCell(position);
TileInfoRegion.TileInfo tileInfo = map.getMapTileState().get(tilePos.getX(), tilePos.getY());
int lastChunkHash = map.getMapChunkState().get(chunkPos.getX(), chunkPos.getY());
int currentChunkHash = 0;
class FindHashConsumer implements ChunkConsumer.ListOnly {
public int timestamp = 0;
@Override
public void accept(int chunkX, int chunkZ, int timestamp) {
if (chunkPos.getX() == chunkX && chunkPos.getY() == chunkZ)
this.timestamp = timestamp;
}
}
try {
FindHashConsumer findHashConsumer = new FindHashConsumer();
map.getWorld().getRegion(regionPos.getX(), regionPos.getY())
.iterateAllChunks(findHashConsumer);
currentChunkHash = findHashConsumer.timestamp;
} catch (IOException e) {
Logger.global.logError("Failed to load chunk-hash.", e);
}
Map<String, Object> lines = new LinkedHashMap<>();
lines.put("region-pos", regionPos);
lines.put("chunk-pos", chunkPos);
lines.put("chunk-curr-hash", currentChunkHash);
lines.put("chunk-last-hash", lastChunkHash);
lines.put("tile-pos", tilePos);
lines.put("tile-render-time", tileInfo.getRenderTime());
lines.put("tile-state", tileInfo.getState().getKey().getFormatted());
source.sendMessage(Text.of(TextColor.GOLD, "Map tile info:"));
source.sendMessage(formatMap(lines));
}, "BlueMap-Plugin-DebugMapCommand").start();
return 1;
}
public int debugBlockCommand(CommandContext<S> context) {
final CommandSource source = commandSourceInterface.apply(context.getSource());
@ -523,7 +609,7 @@ private Text formatBlock(Block<?> block) {
lines.put("chunk-has-lightdata", chunk.hasLightData());
lines.put("chunk-inhabited-time", chunk.getInhabitedTime());
lines.put("block-state", block.getBlockState());
lines.put("biome", block.getBiomeId());
lines.put("biome", block.getBiome().getKey());
lines.put("position", block.getX() + " | " + block.getY() + " | " + block.getZ());
lines.put("block-light", block.getBlockLightLevel());
lines.put("sun-light", block.getSunLightLevel());
@ -533,6 +619,10 @@ private Text formatBlock(Block<?> block) {
lines.put("block-entity", blockEntity);
}
return formatMap(lines);
}
private Text formatMap(Map<String, Object> lines) {
Object[] textElements = lines.entrySet().stream()
.flatMap(e -> Stream.of(TextColor.GRAY, e.getKey(), ": ", TextColor.WHITE, e.getValue(), "\n"))
.toArray(Object[]::new);
@ -754,14 +844,9 @@ public int updateCommand(CommandContext<S> context, boolean force) {
}
for (BmMap map : maps) {
MapUpdateTask updateTask = new MapUpdateTask(map, center, radius);
MapUpdateTask updateTask = new MapUpdateTask(map, center, radius, s -> force);
plugin.getRenderManager().scheduleRenderTask(updateTask);
if (force) {
MapRenderState state = map.getRenderState();
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)"));
}
@ -874,7 +959,7 @@ public int mapsCommand(CommandContext<S> context) {
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0World: ",
TextColor.DARK_GRAY, map.getWorld().getId()));
lines.add(Text.of(TextColor.GRAY, "\u00A0\u00A0\u00A0Last Update: ",
TextColor.DARK_GRAY, helper.formatTime(map.getRenderState().getLatestRenderTime())));
TextColor.DARK_GRAY, helper.formatTime(map.getMapTileState().getLastRenderTime() * 1000L)));
if (frozen)
lines.add(Text.of(TextColor.AQUA, TextFormat.ITALIC, "\u00A0\u00A0\u00A0This map is frozen!"));

View File

@ -84,13 +84,10 @@ public void cancel() {
public boolean contains(RenderTask task) {
if (this.equals(task)) return true;
if (task instanceof CombinedRenderTask) {
CombinedRenderTask<?> combinedTask = (CombinedRenderTask<?>) task;
if (task instanceof CombinedRenderTask<?> combinedTask) {
for (RenderTask subTask : combinedTask.tasks) {
if (!this.contains(subTask)) return false;
}
return true;
}
@ -111,4 +108,5 @@ public Optional<String> getDetail() {
if (this.currentTaskIndex >= this.tasks.size()) return Optional.empty();
return Optional.ofNullable(this.tasks.get(this.currentTaskIndex).getDescription());
}
}

View File

@ -55,19 +55,15 @@ public void doWork() throws Exception {
// save lowres-tile-manager to clear/flush any buffered data
this.map.getLowresTileManager().save();
try {
// purge the map
map.getStorage().delete(progress -> {
this.progress = progress;
return !this.cancelled;
});
// reset texture gallery
map.resetTextureGallery();
} finally {
// reset renderstate
map.getRenderState().reset();
}
map.getMapTileState().reset();
map.getMapChunkState().reset();
}
@Override

View File

@ -26,16 +26,18 @@
import com.flowpowered.math.vector.Vector2i;
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.map.renderstate.MapTileState;
import de.bluecolored.bluemap.core.map.renderstate.TileState;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.io.IOException;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@DebugDump
public class MapUpdateTask extends CombinedRenderTask<RenderTask> {
@ -47,7 +49,7 @@ public MapUpdateTask(BmMap map) {
this(map, getRegions(map));
}
public MapUpdateTask(BmMap map, boolean force) {
public MapUpdateTask(BmMap map, Predicate<TileState> force) {
this(map, getRegions(map), force);
}
@ -55,15 +57,15 @@ public MapUpdateTask(BmMap map, Vector2i center, int radius) {
this(map, getRegions(map, center, radius));
}
public MapUpdateTask(BmMap map, Vector2i center, int radius, boolean force) {
public MapUpdateTask(BmMap map, Vector2i center, int radius, Predicate<TileState> force) {
this(map, getRegions(map, center, radius), force);
}
public MapUpdateTask(BmMap map, Collection<Vector2i> regions) {
this(map, regions, false);
this(map, regions, s -> false);
}
public MapUpdateTask(BmMap map, Collection<Vector2i> regions, boolean force) {
public MapUpdateTask(BmMap map, Collection<Vector2i> regions, Predicate<TileState> force) {
super("Update map '" + map.getId() + "'", createTasks(map, regions, force));
this.map = map;
this.regions = Collections.unmodifiableCollection(new ArrayList<>(regions));
@ -77,7 +79,7 @@ public Collection<Vector2i> getRegions() {
return regions;
}
private static Collection<RenderTask> createTasks(BmMap map, Collection<Vector2i> regions, boolean force) {
private static Collection<RenderTask> createTasks(BmMap map, Collection<Vector2i> regions, Predicate<TileState> force) {
ArrayList<WorldRegionRenderTask> regionTasks = new ArrayList<>(regions.size());
regions.forEach(region -> regionTasks.add(new WorldRegionRenderTask(map, region, force)));
@ -99,33 +101,47 @@ private static Collection<RenderTask> createTasks(BmMap map, Collection<Vector2i
return tasks;
}
private static List<Vector2i> getRegions(BmMap map) {
private static Collection<Vector2i> getRegions(BmMap map) {
return getRegions(map, null, -1);
}
private static List<Vector2i> getRegions(BmMap map, Vector2i center, int radius) {
private static Collection<Vector2i> getRegions(BmMap map, Vector2i center, int radius) {
World world = map.getWorld();
Grid regionGrid = world.getRegionGrid();
Predicate<Vector2i> regionFilter = map.getMapSettings().getRenderBoundariesCellFilter(regionGrid);
Predicate<Vector2i> regionBoundsFilter = map.getMapSettings().getCellRenderBoundariesFilter(regionGrid, true);
Predicate<Vector2i> regionRadiusFilter;
if (center == null || radius < 0) {
return world.listRegions().stream()
.filter(regionFilter)
.collect(Collectors.toList());
}
List<Vector2i> regions = new ArrayList<>();
regionRadiusFilter = r -> true;
} else {
Vector2i halfCell = regionGrid.getGridSize().div(2);
long increasedRadiusSquared = (long) Math.pow(radius + Math.ceil(halfCell.length()), 2);
for (Vector2i region : world.listRegions()) {
if (!regionFilter.test(region)) continue;
Vector2i min = regionGrid.getCellMin(region);
regionRadiusFilter = r -> {
Vector2i min = regionGrid.getCellMin(r);
Vector2i regionCenter = min.add(halfCell);
return regionCenter.toLong().distanceSquared(center.toLong()) <= increasedRadiusSquared;
};
}
if (regionCenter.toLong().distanceSquared(center.toLong()) <= increasedRadiusSquared)
regions.add(region);
Set<Vector2i> regions = new HashSet<>();
// update all regions in the world-files
world.listRegions().stream()
.filter(regionBoundsFilter)
.filter(regionRadiusFilter)
.forEach(regions::add);
// also update regions that are present as map-tile-state files (they might have been rendered before but deleted now)
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid cellGrid = MapTileState.GRID.multiply(tileGrid);
try (Stream<GridStorage.Cell> stream = map.getStorage().tileState().stream()) {
stream
.map(c -> new Vector2i(c.getX(), c.getZ()))
.flatMap(v -> cellGrid.getIntersecting(v, regionGrid).stream())
.filter(regionRadiusFilter)
.forEach(regions::add);
} catch (IOException ex) {
Logger.global.logError("Failed to load map tile state!", ex);
}
return regions;

View File

@ -106,9 +106,23 @@ public boolean isRunning() {
}
public void awaitIdle() throws InterruptedException {
awaitIdle(false);
}
public void awaitIdle(boolean log) throws InterruptedException {
synchronized (this.renderTasks) {
while (!this.renderTasks.isEmpty())
this.renderTasks.wait(10000);
while (!this.renderTasks.isEmpty()) {
this.renderTasks.wait(5000);
if (log) {
RenderTask task = this.getCurrentRenderTask();
if (task != null) {
Logger.global.logInfo("Waiting for task '" + task.getDescription() + "' to stop.. (" +
(Math.round(task.estimateProgress() * 10000) / 100.0) + "%)");
}
}
}
}
}

View File

@ -29,205 +29,246 @@
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.map.renderstate.TileActionResolver.ActionAndNextState;
import de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.BoundsSituation;
import de.bluecolored.bluemap.core.map.renderstate.TileInfoRegion;
import de.bluecolored.bluemap.core.map.renderstate.TileState;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.Action.DELETE;
import static de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.Action.RENDER;
@DebugDump
public class WorldRegionRenderTask implements RenderTask {
private final BmMap map;
private final Vector2i worldRegion;
private final boolean force;
@Getter private final BmMap map;
@Getter private final Vector2i regionPos;
@Getter private final Predicate<TileState> force;
private Deque<Vector2i> tiles;
private int tileCount;
private long startTime;
private Grid regionGrid, chunkGrid, tileGrid;
private Vector2i chunkMin, chunkMax, chunksSize;
private Vector2i tileMin, tileMax, tileSize;
private int[] chunkHashes;
private ActionAndNextState[] tileActions;
private volatile int nextTileX, nextTileZ;
private volatile int atWork;
private volatile boolean cancelled;
private volatile boolean completed, cancelled;
public WorldRegionRenderTask(BmMap map, Vector2i worldRegion) {
this(map, worldRegion, false);
public WorldRegionRenderTask(BmMap map, Vector2i regionPos) {
this(map, regionPos, false);
}
public WorldRegionRenderTask(BmMap map, Vector2i worldRegion, boolean force) {
public WorldRegionRenderTask(BmMap map, Vector2i regionPos, boolean force) {
this(map, regionPos, s -> force);
}
public WorldRegionRenderTask(BmMap map, Vector2i regionPos, Predicate<TileState> force) {
this.map = map;
this.worldRegion = worldRegion;
this.regionPos = regionPos;
this.force = force;
this.tiles = null;
this.tileCount = -1;
this.startTime = -1;
this.nextTileX = 0;
this.nextTileZ = 0;
this.atWork = 0;
this.completed = false;
this.cancelled = false;
}
private synchronized void init() {
Set<Vector2l> tileSet = new HashSet<>();
startTime = System.currentTimeMillis();
// collect chunks
long changesSince = force ? 0 : map.getRenderState().getRenderTime(worldRegion);
Region region = map.getWorld().getRegion(worldRegion.getX(), worldRegion.getY());
Collection<Vector2i> chunks = new ArrayList<>(1024);
// calculate bounds
this.regionGrid = map.getWorld().getRegionGrid();
this.chunkGrid = map.getWorld().getChunkGrid();
this.tileGrid = map.getHiresModelManager().getTileGrid();
this.chunkMin = regionGrid.getCellMin(regionPos, chunkGrid);
this.chunkMax = regionGrid.getCellMax(regionPos, chunkGrid);
this.chunksSize = chunkMax.sub(chunkMin).add(1, 1);
this.tileMin = regionGrid.getCellMin(regionPos, tileGrid);
this.tileMax = regionGrid.getCellMax(regionPos, tileGrid);
this.tileSize = tileMax.sub(tileMin).add(1, 1);
// load chunk-hash array
int chunkMaxCount = chunksSize.getX() * chunksSize.getY();
try {
region.iterateAllChunks((ChunkConsumer.ListOnly) (x, z, timestamp) -> {
if (timestamp >= changesSince) chunks.add(new Vector2i(x, z));
chunkHashes = new int[chunkMaxCount];
map.getWorld().getRegion(regionPos.getX(), regionPos.getY())
.iterateAllChunks( (ChunkConsumer.ListOnly) (x, z, timestamp) -> {
chunkHashes[chunkIndex(
x - chunkMin.getX(),
z - chunkMin.getY()
)] = timestamp;
map.getWorld().invalidateChunkCache(x, z);
});
} catch (IOException ex) {
Logger.global.logWarning("Failed to read region " + worldRegion + " from world " + map.getWorld().getName() + " (" + ex + ")");
Logger.global.logError("Failed to load chunks for region " + regionPos, ex);
cancel();
}
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
Predicate<Vector2i> boundsTileFilter = map.getMapSettings().getRenderBoundariesCellFilter(tileGrid);
// check tile actions
int tileMaxCount = tileSize.getX() * tileSize.getY();
int tileRenderCount = 0;
int tileDeleteCount = 0;
tileActions = new ActionAndNextState[tileMaxCount];
for (int x = 0; x < tileSize.getX(); x++) {
for (int z = 0; z < tileSize.getY(); z++) {
Vector2i tile = new Vector2i(tileMin.getX() + x, tileMin.getY() + z);
TileState tileState = map.getMapTileState().get(tile.getX(), tile.getY()).getState();
for (Vector2i chunk : chunks) {
Vector2i tileMin = chunkGrid.getCellMin(chunk, tileGrid);
Vector2i tileMax = chunkGrid.getCellMax(chunk, tileGrid);
int tileIndex = tileIndex(x, z);
tileActions[tileIndex] = tileState.findActionAndNextState(
force.test(tileState) || checkChunksHaveChanges(tile),
checkTileBounds(tile)
);
for (int x = tileMin.getX(); x <= tileMax.getX(); x++) {
for (int z = tileMin.getY(); z <= tileMax.getY(); z++) {
tileSet.add(new Vector2l(x, z));
if (tileActions[tileIndex].action() == RENDER)
tileRenderCount++;
if (tileActions[tileIndex].action() == DELETE)
tileDeleteCount++;
}
}
// make sure chunk gets re-loaded from disk
map.getWorld().invalidateChunkCache(chunk.getX(), chunk.getY());
}
if (tileRenderCount >= tileMaxCount * 0.75)
map.getWorld().preloadRegionChunks(regionPos.getX(), regionPos.getY());
this.tileCount = tileSet.size();
this.tiles = tileSet.stream()
.sorted(WorldRegionRenderTask::compareVec2L) //sort with longs to avoid overflow (comparison uses distanceSquared)
.map(Vector2l::toInt) // back to ints
.filter(boundsTileFilter)
.filter(map.getTileFilter())
.collect(Collectors.toCollection(ArrayDeque::new));
if (tileRenderCount + tileDeleteCount == 0)
completed = true;
if (tiles.isEmpty()) complete();
else {
// preload chunks
map.getWorld().preloadRegionChunks(worldRegion.getX(), worldRegion.getY());
}
}
@Override
public void doWork() {
if (cancelled) return;
if (cancelled || completed) return;
Vector2i tile;
int tileX, tileZ;
synchronized (this) {
if (tiles == null) init();
if (tiles.isEmpty()) return;
if (cancelled || completed) return;
tile = tiles.pollFirst();
tileX = nextTileX;
tileZ = nextTileZ;
if (tileX == 0 && tileZ == 0) {
init();
if (cancelled || completed) return;
}
nextTileX = tileX + 1;
if (nextTileX >= tileSize.getX()) {
nextTileZ = tileZ + 1;
nextTileX = 0;
}
if (nextTileZ >= tileSize.getY()) {
completed = true;
}
this.atWork++;
}
if (tileRenderPreconditions(tile)) {
map.renderTile(tile); // <- actual work
}
processTile(tileX, tileZ);
synchronized (this) {
this.atWork--;
if (atWork <= 0 && tiles.isEmpty() && !cancelled) {
if (atWork <= 0 && completed && !cancelled) {
complete();
}
}
}
private boolean tileRenderPreconditions(Vector2i tile) {
Grid tileGrid = map.getHiresModelManager().getTileGrid();
Grid chunkGrid = map.getWorld().getChunkGrid();
private void processTile(int x, int z) {
Vector2i tile = new Vector2i(tileMin.getX() + x, tileMin.getY() + z);
ActionAndNextState action = tileActions[tileIndex(x, z)];
TileState resultState = TileState.RENDER_ERROR;
Vector2i minChunk = tileGrid.getCellMin(tile, chunkGrid);
Vector2i maxChunk = tileGrid.getCellMax(tile, chunkGrid);
try {
long minInhab = map.getMapSettings().getMinInhabitedTime();
int minInhabRadius = map.getMapSettings().getMinInhabitedTimeRadius();
if (minInhabRadius < 0) minInhabRadius = 0;
if (minInhabRadius > 16) minInhabRadius = 16; // sanity check
boolean isInhabited = false;
resultState = switch (action.action()) {
for (int x = minChunk.getX(); x <= maxChunk.getX(); x++) {
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;
}
case NONE -> action.state();
case RENDER -> {
TileState failedState = checkTileRenderPreconditions(tile);
if (failedState != null){
map.unrenderTile(tile);
yield failedState;
}
if (minInhabRadius > 0 && !isInhabited) {
for (int x = minChunk.getX() - minInhabRadius; x <= maxChunk.getX() + minInhabRadius; x++) {
for (int z = minChunk.getY() - minInhabRadius; z <= maxChunk.getY() + minInhabRadius; z++) {
Chunk chunk = map.getWorld().getChunk(x, z);
if (chunk.getInhabitedTime() >= minInhab) {
isInhabited = true;
break;
}
}
}
map.renderTile(tile);
yield action.state();
}
return isInhabited;
case DELETE -> {
map.unrenderTile(tile);
yield action.state();
}
private void complete() {
map.getRenderState().setRenderTime(worldRegion, startTime);
};
} catch (Exception ex) {
Logger.global.logError("Error while processing map-tile " + tile + " for map '" + map.getId() + "'", ex);
} finally {
// mark tile with new state
map.getMapTileState().set(tile.getX(), tile.getY(), new TileInfoRegion.TileInfo(
(int) (System.currentTimeMillis() / 1000),
resultState
));
}
}
private synchronized void complete() {
// save chunk-hashes
if (chunkHashes != null) {
for (int x = 0; x < chunksSize.getX(); x++) {
for (int z = 0; z < chunksSize.getY(); z++) {
int hash = chunkHashes[chunkIndex(x, z)];
map.getMapChunkState().set(chunkMin.getX() + x, chunkMin.getY() + z, hash);
}
}
chunkHashes = null;
}
// save map (at most, every minute)
map.save(TimeUnit.MINUTES.toMillis(1));
}
@Override
@DebugDump
public synchronized boolean hasMoreWork() {
return !cancelled && (tiles == null || !tiles.isEmpty());
return !completed && !cancelled;
}
@Override
@DebugDump
public double estimateProgress() {
if (tiles == null) return 0;
if (tileCount == 0) return 1;
double remainingTiles = tiles.size();
return 1 - (remainingTiles / this.tileCount);
if (tileSize == null) return 0;
return Math.min((double) (nextTileZ * tileSize.getX() + nextTileX) / (tileSize.getX() * tileSize.getY()), 1);
}
@Override
public void cancel() {
this.cancelled = true;
synchronized (this) {
if (tiles != null) this.tiles.clear();
}
}
public BmMap getMap() {
return map;
}
public Vector2i getWorldRegion() {
return worldRegion;
}
public boolean isForce() {
return force;
}
@Override
public String getDescription() {
return "Update region " + getWorldRegion() + " for map '" + map.getId() + "'";
return "Update region " + regionPos + " for map '" + map.getId() + "'";
}
@Override
@ -235,19 +276,101 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WorldRegionRenderTask that = (WorldRegionRenderTask) o;
return force == that.force && map.getId().equals(that.map.getId()) && worldRegion.equals(that.worldRegion);
return force == that.force && map.getId().equals(that.map.getId()) && regionPos.equals(that.regionPos);
}
@Override
public int hashCode() {
return worldRegion.hashCode();
return regionPos.hashCode();
}
private int chunkIndex(int x, int z) {
return z * chunksSize.getX() + x;
}
private int tileIndex(int x, int z) {
return z * tileSize.getX() + x;
}
private boolean checkChunksHaveChanges(Vector2i tile) {
int minX = tileGrid.getCellMinX(tile.getX(), chunkGrid),
maxX = tileGrid.getCellMaxX(tile.getX(), chunkGrid),
minZ = tileGrid.getCellMinY(tile.getY(), chunkGrid),
maxZ = tileGrid.getCellMaxY(tile.getY(), chunkGrid);
for (int chunkX = minX; chunkX <= maxX; chunkX++) {
for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) {
int dx = chunkX - chunkMin.getX();
int dz = chunkZ - chunkMin.getY();
// only check hash for chunks inside the current region
if (
chunkX >= chunkMin.getX() && chunkX <= chunkMax.getX() &&
chunkZ >= chunkMin.getY() && chunkZ <= chunkMax.getY()
) {
int hash = chunkHashes[chunkIndex(dx, dz)];
int lastHash = map.getMapChunkState().get(chunkX, chunkZ);
if (lastHash != hash) return true;
}
}
}
return false;
}
private BoundsSituation checkTileBounds(Vector2i tile) {
boolean isInsideBounds = map.getMapSettings().isInsideRenderBoundaries(tile, tileGrid, true);
if (!isInsideBounds) return BoundsSituation.OUTSIDE;
boolean isFullyInsideBounds = map.getMapSettings().isInsideRenderBoundaries(tile, tileGrid, false);
return isFullyInsideBounds ? BoundsSituation.INSIDE : BoundsSituation.EDGE;
}
private @Nullable TileState checkTileRenderPreconditions(Vector2i tile) {
boolean chunksAreInhabited = false;
long minInhabitedTime = map.getMapSettings().getMinInhabitedTime();
int minInhabitedTimeRadius = map.getMapSettings().getMinInhabitedTimeRadius();
boolean requireLight = !map.getMapSettings().isIgnoreMissingLightData();
int minX = tileGrid.getCellMinX(tile.getX(), chunkGrid),
maxX = tileGrid.getCellMaxX(tile.getX(), chunkGrid),
minZ = tileGrid.getCellMinY(tile.getY(), chunkGrid),
maxZ = tileGrid.getCellMaxY(tile.getY(), chunkGrid);
for (int chunkX = minX; chunkX <= maxX; chunkX++) {
for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) {
Chunk chunk = map.getWorld().getChunk(chunkX, chunkZ);
if (chunk == Chunk.ERRORED_CHUNK) return TileState.CHUNK_ERROR;
if (!chunk.isGenerated()) return TileState.NOT_GENERATED;
if (requireLight && !chunk.hasLightData()) return TileState.MISSING_LIGHT;
if (chunk.getInhabitedTime() >= minInhabitedTime) chunksAreInhabited = true;
}
}
// second pass for increased inhabited-time-radius
if (!chunksAreInhabited && minInhabitedTimeRadius > 0) {
inhabitedRadiusCheck:
for (int chunkX = minX - minInhabitedTimeRadius; chunkX <= maxX + minInhabitedTimeRadius; chunkX++) {
for (int chunkZ = minZ - minInhabitedTimeRadius; chunkZ <= maxZ + minInhabitedTimeRadius; chunkZ++) {
Chunk chunk = map.getWorld().getChunk(chunkX, chunkZ);
if (chunk.getInhabitedTime() >= minInhabitedTime) {
chunksAreInhabited = true;
break inhabitedRadiusCheck;
}
}
}
}
return chunksAreInhabited ? null : TileState.LOW_INHABITED_TIME;
}
public static Comparator<WorldRegionRenderTask> defaultComparator(final Vector2i centerRegion) {
return (task1, task2) -> {
// use long to compare to avoid overflow (comparison uses distanceSquared)
Vector2l task1Rel = new Vector2l(task1.worldRegion.getX() - centerRegion.getX(), task1.worldRegion.getY() - centerRegion.getY());
Vector2l task2Rel = new Vector2l(task2.worldRegion.getX() - centerRegion.getX(), task2.worldRegion.getY() - centerRegion.getY());
Vector2l task1Rel = new Vector2l(task1.regionPos.getX() - centerRegion.getX(), task1.regionPos.getY() - centerRegion.getY());
Vector2l task2Rel = new Vector2l(task2.regionPos.getX() - centerRegion.getX(), task2.regionPos.getY() - centerRegion.getY());
return compareVec2L(task1Rel, task2Rel);
};
}

View File

@ -25,10 +25,10 @@
package de.bluecolored.bluemap.common.serverinterface;
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 org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Collection;
@ -37,7 +37,7 @@
public interface Server {
@DebugDump
MinecraftVersion getMinecraftVersion();
@Nullable String getMinecraftVersion();
/**
* Returns the Folder containing the configurations for the plugin

View File

@ -27,8 +27,10 @@
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.core.logger.Logger;
import lombok.Getter;
@DebugDump
@Getter
public class LoggingRequestHandler implements HttpRequestHandler {
private final HttpRequestHandler delegate;

View File

@ -29,6 +29,10 @@
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 lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.intellij.lang.annotations.Language;
import java.util.LinkedList;
@ -38,7 +42,7 @@
@DebugDump
public class RoutingRequestHandler implements HttpRequestHandler {
public LinkedList<Route> routes;
public final LinkedList<Route> routes;
public RoutingRequestHandler() {
this.routes = new LinkedList<>();
@ -80,36 +84,20 @@ public HttpResponse handle(HttpRequest request) {
}
@DebugDump
private static class Route {
@AllArgsConstructor
@Getter @Setter
public static class Route {
private final Pattern routePattern;
private final HttpRequestHandler handler;
private final String replacementRoute;
private @NonNull Pattern routePattern;
private @NonNull String replacementRoute;
private @NonNull HttpRequestHandler handler;
public Route(Pattern routePattern, HttpRequestHandler handler) {
public Route(@NonNull Pattern routePattern, @NonNull HttpRequestHandler handler) {
this.routePattern = routePattern;
this.replacementRoute = "$0";
this.handler = handler;
}
public Route(Pattern routePattern, String replacementRoute, HttpRequestHandler handler) {
this.routePattern = routePattern;
this.replacementRoute = replacementRoute;
this.handler = handler;
}
public Pattern getRoutePattern() {
return routePattern;
}
public HttpRequestHandler getHandler() {
return handler;
}
public String getReplacementRoute() {
return replacementRoute;
}
}
}

View File

@ -25,13 +25,16 @@
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.api.debug.DebugDump;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
@DebugDump
public class HttpServer extends Server {
private final HttpRequestHandler requestHandler;
@Getter @Setter
private HttpRequestHandler requestHandler;
public HttpServer(HttpRequestHandler requestHandler) throws IOException {
this.requestHandler = requestHandler;
@ -40,10 +43,6 @@ public HttpServer(HttpRequestHandler requestHandler) throws IOException {
@Override
public SelectionConsumer createConnectionHandler() {
return new HttpConnection(requestHandler);
// Enable async request handling ...
// TODO: maybe find a better/separate executor than using bluemap's common thread-pool
//return new HttpConnection(requestHandler, BlueMap.THREAD_POOL);
}
}

View File

@ -24,6 +24,9 @@
*/
package de.bluecolored.bluemap.common.web.http;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum HttpStatusCode {
CONTINUE (100, "Continue"),
@ -47,13 +50,8 @@ public enum HttpStatusCode {
SERVICE_UNAVAILABLE (503, "Service Unavailable"),
HTTP_VERSION_NOT_SUPPORTED (505, "HTTP Version not supported");
private int code;
private String message;
private HttpStatusCode(int code, String message) {
this.code = code;
this.message = message;
}
private final int code;
private final String message;
public int getCode(){
return code;

View File

@ -5,7 +5,7 @@
# By changing the setting (accept-download) below to TRUE you are indicating that you have accepted mojang's EULA (https://account.mojang.com/documents/minecraft_eula),
# you confirm that you own a license to Minecraft (Java Edition)
# and you agree that BlueMap will download and use a minecraft-client file (depending on the minecraft-version) from mojangs servers (https://launcher.mojang.com/) for you.
# and you agree that BlueMap will download and use a minecraft-client file (depending on the minecraft-version) from mojangs servers (https://piston-meta.mojang.com/) for you.
# This file contains resources that belong to mojang and you must not redistribute it or do anything else that is not compliant with mojang's EULA.
# BlueMap uses resources in this file to generate the 3D-Models used for the map and texture them. (BlueMap will not work without those resources.)
# ${timestamp}

View File

@ -14,7 +14,9 @@ root: "${root}"
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - GZIP
# - NONE
# The default is: GZIP
# - gzip
# - zstd
# - deflate
# - none
# The default is: gzip
compression: gzip

View File

@ -40,6 +40,8 @@ max-connections: -1
# The compression-type that bluemap will use to compress generated map-data.
# Available compression-types are:
# - gzip
# - zstd
# - deflate
# - none
# The default is: gzip
compression: gzip

View File

@ -9,30 +9,27 @@ $username = 'root';
$password = '';
$database = 'bluemap';
// set this to "none" if you disabled compression on your maps
$hiresCompression = 'gzip';
// !!! END - DONT CHANGE ANYTHING AFTER THIS LINE !!!
// compression
$compressionHeaderMap = [
"bluemap:none" => null,
"bluemap:gzip" => "gzip",
"bluemap:deflate" => "deflate",
"bluemap:zstd" => "zstd",
"bluemap:lz4" => "lz4"
]
// some helper functions
function error($code, $message = null) {
global $path;
http_response_code($code);
header("Content-Type: text/plain");
echo "BlueMap php-script - $code\n";
if ($message != null) echo $message."\n";
echo "Requested Path: $path";
exit;
}
function startsWith($haystack, $needle) {
return substr($haystack, 0, strlen($needle)) === $needle;
}
// meta files
$metaFileKeys = [
"settings.json" => "bluemap:settings",
"textures.json" => "bluemap:textures",
"live/markers.json" => "bluemap:markers",
"live/players.json" => "bluemap:players",
]
// mime-types for meta-files
$mimeDefault = "application/octet-stream";
@ -70,6 +67,34 @@ $mimeTypes = [
"woff2" => "font/woff2"
];
// some helper functions
function error($code, $message = null) {
global $path;
http_response_code($code);
header("Content-Type: text/plain");
echo "BlueMap php-script - $code\n";
if ($message != null) echo $message."\n";
echo "Requested Path: $path";
exit;
}
function startsWith($haystack, $needle) {
return substr($haystack, 0, strlen($needle)) === $needle;
}
function issetOrElse(& $var, $fallback) {
return isset($var) ? $var : $fallback;
}
function compressionHeader($compressionKey) {
global $compressionHeaderMap;
$compressionHeader = issetOrElse($compressionHeaderMap[$compressionKey], null);
if ($compressionHeader)
header("Content-Encoding: ".$compressionHeader);
}
function getMimeType($path) {
global $mimeDefault, $mimeTypes;
@ -122,55 +147,55 @@ if (startsWith($path, "/maps/")) {
// Initialize PDO
try {
$sql = new PDO("$driver:host=$hostname;dbname=$database", $username, $password);
$sql = new PDO("$driver:host=$hostname;port=$port;dbname=$database", $username, $password);
$sql->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e ) { error(500, "Failed to connect to database"); }
// provide map-tiles
if (startsWith($mapPath, "tiles/")) {
// parse tile-coordinates
preg_match_all("/tiles\/([\d\/]+)\/x(-?[\d\/]+)z(-?[\d\/]+).*/", $mapPath, $matches);
$lod = intval($matches[1][0]);
$storage = $lod === 0 ? "bluemap:hires" : "bluemap:lowres/".$lod
$tileX = intval(str_replace("/", "", $matches[2][0]));
$tileZ = intval(str_replace("/", "", $matches[3][0]));
$compression = $lod === 0 ? $hiresCompression : "none";
// query for tile
try {
$statement = $sql->prepare("
SELECT t.data
FROM bluemap_map_tile t
SELECT d.data, c.compression
FROM bluemap_grid_storage_data d
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
ON d.map = m.id
INNER JOIN bluemap_grid_storage s
ON d.storage = s.id
INNER JOIN bluemap_compression c
ON d.compression = c.id
WHERE m.map_id = :map_id
AND t.lod = :lod
AND t.x = :x
AND t.z = :z
AND c.compression = :compression
AND s.key = :storage
AND d.x = :x
AND d.z = :z
");
$statement->bindParam( ':map_id', $mapId, PDO::PARAM_STR );
$statement->bindParam( ':lod', $lod, PDO::PARAM_INT );
$statement->bindParam( ':storage', $storage, PDO::PARAM_STR );
$statement->bindParam( ':x', $tileX, PDO::PARAM_INT );
$statement->bindParam( ':z', $tileZ, PDO::PARAM_INT );
$statement->bindParam( ':compression', $compression, PDO::PARAM_STR);
$statement->bindParam( ':compression', $compression, PDO::PARAM_STR );
$statement->setFetchMode(PDO::FETCH_ASSOC);
$statement->execute();
// return result
if ($line = $statement->fetch()) {
header("Cache-Control: public,max-age=86400");
compressionHeader($line["compression"]);
if ($compression !== "none")
header("Content-Encoding: $compression");
if ($lod === 0) {
header("Content-Type: application/octet-stream");
} else {
header("Content-Type: image/png");
}
send($line["data"]);
exit;
}
@ -183,27 +208,39 @@ if (startsWith($path, "/maps/")) {
}
// provide meta-files
$storage = issetOrElse($metaFileKeys[$mapPath], null);
if ($storage === null && startsWith($mapPath, "assets/"))
$storage = "bluemap:asset/".substr($mapPath, strlen("assets/"));
if ($storage !== null) {
try {
$statement = $sql->prepare("
SELECT t.value
FROM bluemap_map_meta t
SELECT d.data, c.compression
FROM bluemap_item_storage_data d
INNER JOIN bluemap_map m
ON t.map = m.id
ON d.map = m.id
INNER JOIN bluemap_item_storage s
ON d.storage = s.id
INNER JOIN bluemap_compression c
ON d.compression = c.id
WHERE m.map_id = :map_id
AND t.key = :map_path
AND s.key = :storage
");
$statement->bindParam( ':map_id', $mapId, PDO::PARAM_STR );
$statement->bindParam( ':map_path', $mapPath, PDO::PARAM_STR );
$statement->bindParam( ':storage', $storage, PDO::PARAM_STR );
$statement->setFetchMode(PDO::FETCH_ASSOC);
$statement->execute();
if ($line = $statement->fetch()) {
header("Cache-Control: public,max-age=86400");
header("Content-Type: ".getMimeType($mapPath));
send($line["value"]);
compressionHeader($line["compression"]);
send($line["data"]);
exit;
}
} catch (PDOException $e) { error(500, "Failed to fetch data"); }
}
}

View File

@ -65,7 +65,7 @@ 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 ("de.bluecolored.bluenbt:BlueNBT:2.2.1")
api ("de.bluecolored.bluenbt:BlueNBT:2.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")

View File

@ -1,181 +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;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.util.Lazy;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@DebugDump
public class MinecraftVersion implements Comparable<MinecraftVersion> {
private static final Pattern VERSION_REGEX = Pattern.compile("(?<major>\\d+)\\.(?<minor>\\d+)(?:\\.(?<patch>\\d+))?(?:-(?:pre|rc)\\d+)?");
public static final MinecraftVersion LATEST_SUPPORTED = new MinecraftVersion(1, 20, 3);
public static final MinecraftVersion EARLIEST_SUPPORTED = new MinecraftVersion(1, 13);
private final int major, minor, patch;
private final Lazy<MinecraftResource> resource;
public MinecraftVersion(int major, int minor) {
this(major, minor, 0);
}
public MinecraftVersion(int major, int minor, int patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.resource = new Lazy<>(this::findBestMatchingResource);
}
public String getVersionString() {
return major + "." + minor + "." + patch;
}
public MinecraftResource getResource() {
return this.resource.getValue();
}
public boolean isAtLeast(MinecraftVersion minVersion) {
return compareTo(minVersion) >= 0;
}
public boolean isAtMost(MinecraftVersion maxVersion) {
return compareTo(maxVersion) <= 0;
}
public boolean isBefore(MinecraftVersion minVersion) {
return compareTo(minVersion) < 0;
}
public boolean isAfter(MinecraftVersion minVersion) {
return compareTo(minVersion) > 0;
}
@Override
public int compareTo(MinecraftVersion other) {
int result;
result = Integer.compare(major, other.major);
if (result != 0) return result;
result = Integer.compare(minor, other.minor);
if (result != 0) return result;
result = Integer.compare(patch, other.patch);
return result;
}
public boolean majorEquals(MinecraftVersion that) {
return major == that.major;
}
public boolean minorEquals(MinecraftVersion that) {
return major == that.major && minor == that.minor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MinecraftVersion that = (MinecraftVersion) o;
return major == that.major && minor == that.minor && patch == that.patch;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch);
}
private MinecraftResource findBestMatchingResource() {
MinecraftResource[] resources = MinecraftResource.values();
Arrays.sort(resources, Comparator.comparing(MinecraftResource::getVersion).reversed());
for (MinecraftResource resource : resources){
if (isAtLeast(resource.version)) return resource;
}
return resources[resources.length - 1];
}
public static MinecraftVersion of(String versionString) {
Matcher matcher = VERSION_REGEX.matcher(versionString);
if (!matcher.matches()) throw new IllegalArgumentException("Not a valid version string!");
int major = Integer.parseInt(matcher.group("major"));
int minor = Integer.parseInt(matcher.group("minor"));
int patch = 0;
String patchString = matcher.group("patch");
if (patchString != null) patch = Integer.parseInt(patchString);
return new MinecraftVersion(major, minor, patch);
}
@DebugDump
public enum MinecraftResource {
MC_1_13 (new MinecraftVersion(1, 13), "mc1_13", "https://piston-data.mojang.com/v1/objects/30bfe37a8db404db11c7edf02cb5165817afb4d9/client.jar"),
MC_1_14 (new MinecraftVersion(1, 14), "mc1_13", "https://piston-data.mojang.com/v1/objects/8c325a0c5bd674dd747d6ebaa4c791fd363ad8a9/client.jar"),
MC_1_15 (new MinecraftVersion(1, 15), "mc1_15", "https://piston-data.mojang.com/v1/objects/e3f78cd16f9eb9a52307ed96ebec64241cc5b32d/client.jar"),
MC_1_16 (new MinecraftVersion(1, 16), "mc1_16", "https://piston-data.mojang.com/v1/objects/228fdf45541c4c2fe8aec4f20e880cb8fcd46621/client.jar"),
MC_1_16_2 (new MinecraftVersion(1, 16, 2), "mc1_16", "https://piston-data.mojang.com/v1/objects/653e97a2d1d76f87653f02242d243cdee48a5144/client.jar"),
MC_1_17 (new MinecraftVersion(1, 17), "mc1_16", "https://piston-data.mojang.com/v1/objects/1cf89c77ed5e72401b869f66410934804f3d6f52/client.jar"),
MC_1_18 (new MinecraftVersion(1, 18), "mc1_18", "https://piston-data.mojang.com/v1/objects/020aa79e63a7aab5d6f30e5ec7a6c08baee6b64c/client.jar"),
MC_1_19 (new MinecraftVersion(1, 19), "mc1_18", "https://piston-data.mojang.com/v1/objects/a45634ab061beb8c878ccbe4a59c3315f9c0266f/client.jar"),
MC_1_19_4 (new MinecraftVersion(1, 19, 4), "mc1_18", "https://piston-data.mojang.com/v1/objects/958928a560c9167687bea0cefeb7375da1e552a8/client.jar"),
MC_1_20 (new MinecraftVersion(1, 20), "mc1_18", "https://piston-data.mojang.com/v1/objects/e575a48efda46cf88111ba05b624ef90c520eef1/client.jar"),
MC_1_20_3 (new MinecraftVersion(1, 20, 3), "mc1_20_3", "https://piston-data.mojang.com/v1/objects/b178a327a96f2cf1c9f98a45e5588d654a3e4369/client.jar");
private final MinecraftVersion version;
private final String resourcePrefix;
private final String clientUrl;
MinecraftResource(MinecraftVersion version, String resourcePrefix, String clientUrl) {
this.version = version;
this.resourcePrefix = resourcePrefix;
this.clientUrl = clientUrl;
}
public MinecraftVersion getVersion() {
return version;
}
public String getResourcePrefix() {
return resourcePrefix;
}
public String getClientUrl() {
return clientUrl;
}
}
}

View File

@ -34,21 +34,29 @@
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresModelManager;
import de.bluecolored.bluemap.core.map.lowres.LowresTileManager;
import de.bluecolored.bluemap.core.map.renderstate.MapChunkState;
import de.bluecolored.bluemap.core.map.renderstate.MapTileState;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.world.World;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.io.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
@DebugDump
@Getter
public class BmMap {
private static final Gson GSON = ResourcesGson.addAdapter(new GsonBuilder())
@ -63,20 +71,23 @@ public class BmMap {
private final MapSettings mapSettings;
private final ResourcePack resourcePack;
private final MapRenderState renderState;
private final TextureGallery textureGallery;
private final MapTileState mapTileState;
private final MapChunkState mapChunkState;
private final HiresModelManager hiresModelManager;
private final LowresTileManager lowresTileManager;
private final ConcurrentHashMap<String, MarkerSet> markerSets;
private Predicate<Vector2i> tileFilter;
@Setter private Predicate<Vector2i> tileFilter;
private long renderTimeSumNanos;
private long tilesRendered;
@Getter(AccessLevel.NONE) private long renderTimeSumNanos;
@Getter(AccessLevel.NONE) private long tilesRendered;
@Getter(AccessLevel.NONE) private long lastSaveTime;
public BmMap(String id, String name, World world, MapStorage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
public BmMap(String id, String name, World world, MapStorage storage, ResourcePack resourcePack, MapSettings settings) throws IOException, InterruptedException {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.world = Objects.requireNonNull(world);
@ -84,9 +95,14 @@ public BmMap(String id, String name, World world, MapStorage storage, ResourcePa
this.resourcePack = Objects.requireNonNull(resourcePack);
this.mapSettings = Objects.requireNonNull(settings);
this.renderState = new MapRenderState();
loadRenderState();
Logger.global.logDebug("Loading render-state for map '" + id + "'");
this.mapTileState = new MapTileState(storage.tileState());
this.mapTileState.load();
this.mapChunkState = new MapChunkState(storage.chunkState());
if (Thread.interrupted()) throw new InterruptedException();
Logger.global.logDebug("Loading textures for map '" + id + "'");
this.textureGallery = loadTextureGallery();
this.textureGallery.put(resourcePack);
saveTextureGallery();
@ -112,6 +128,7 @@ public BmMap(String id, String name, World world, MapStorage storage, ResourcePa
this.renderTimeSumNanos = 0;
this.tilesRendered = 0;
this.lastSaveTime = -1;
saveMapSettings();
}
@ -130,9 +147,23 @@ public void renderTile(Vector2i tile) {
tilesRendered ++;
}
public void unrenderTile(Vector2i tile) {
hiresModelManager.unrender(tile, lowresTileManager);
}
public synchronized boolean save(long minTimeSinceLastSave) {
long now = System.currentTimeMillis();
if (now - lastSaveTime < minTimeSinceLastSave)
return false;
save();
return true;
}
public synchronized void save() {
lowresTileManager.save();
saveRenderState();
mapTileState.save();
mapChunkState.save();
saveMarkerState();
savePlayerState();
saveMapSettings();
@ -142,25 +173,10 @@ public synchronized void save() {
if (!storage.textures().exists())
saveTextureGallery();
} catch (IOException e) {
Logger.global.logError("Failed to read texture gallery", e);
}
Logger.global.logError("Failed to read texture gallery for map '" + getId() + "'!", e);
}
private void loadRenderState() throws IOException {
try (CompressedInputStream in = storage.renderState().read()){
if (in != null)
this.renderState.load(in.decompress());
} catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);
}
}
public synchronized void saveRenderState() {
try (OutputStream out = storage.renderState().write()) {
this.renderState.save(out);
} catch (IOException ex){
Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex);
}
lastSaveTime = System.currentTimeMillis();
}
private TextureGallery loadTextureGallery() throws IOException {
@ -217,50 +233,6 @@ public synchronized void savePlayerState() {
}
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public World getWorld() {
return world;
}
public MapStorage getStorage() {
return storage;
}
public MapSettings getMapSettings() {
return mapSettings;
}
public MapRenderState getRenderState() {
return renderState;
}
public HiresModelManager getHiresModelManager() {
return hiresModelManager;
}
public LowresTileManager getLowresTileManager() {
return lowresTileManager;
}
public Map<String, MarkerSet> getMarkerSets() {
return markerSets;
}
public Predicate<Vector2i> getTileFilter() {
return tileFilter;
}
public void setTileFilter(Predicate<Vector2i> tileFilter) {
this.tileFilter = tileFilter;
}
public long getAverageNanosPerTile() {
return renderTimeSumNanos / tilesRendered;
}

View File

@ -1,119 +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.map;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.debug.DebugDump;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@DebugDump
public class MapRenderState {
private final Map<Vector2i, Long> regionRenderTimes;
private transient long latestRenderTime = -1;
public MapRenderState() {
regionRenderTimes = new HashMap<>();
}
public synchronized void setRenderTime(Vector2i regionPos, long renderTime) {
regionRenderTimes.put(regionPos, renderTime);
if (latestRenderTime != -1) {
if (renderTime > latestRenderTime)
latestRenderTime = renderTime;
else
latestRenderTime = -1;
}
}
public synchronized long getRenderTime(Vector2i regionPos) {
Long renderTime = regionRenderTimes.get(regionPos);
if (renderTime == null) return -1;
else return renderTime;
}
public long getLatestRenderTime() {
if (latestRenderTime == -1) {
synchronized (this) {
latestRenderTime = regionRenderTimes.values().stream()
.mapToLong(Long::longValue)
.max()
.orElse(-1);
}
}
return latestRenderTime;
}
public synchronized void reset() {
regionRenderTimes.clear();
}
public synchronized void save(OutputStream out) throws IOException {
try (
DataOutputStream dOut = new DataOutputStream(new GZIPOutputStream(out))
) {
dOut.writeInt(regionRenderTimes.size());
for (Map.Entry<Vector2i, Long> entry : regionRenderTimes.entrySet()) {
Vector2i regionPos = entry.getKey();
long renderTime = entry.getValue();
dOut.writeInt(regionPos.getX());
dOut.writeInt(regionPos.getY());
dOut.writeLong(renderTime);
}
dOut.flush();
}
}
public synchronized void load(InputStream in) throws IOException {
regionRenderTimes.clear();
try (
DataInputStream dIn = new DataInputStream(new GZIPInputStream(in))
) {
int size = dIn.readInt();
for (int i = 0; i < size; i++) {
Vector2i regionPos = new Vector2i(
dIn.readInt(),
dIn.readInt()
);
long renderTime = dIn.readLong();
regionRenderTimes.put(regionPos, renderTime);
}
} catch (EOFException ignore){} // ignoring a sudden end of stream, since it is save to only read as many as we can
}
}

View File

@ -32,8 +32,8 @@
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.util.Key;
import org.jetbrains.annotations.Nullable;

View File

@ -29,9 +29,10 @@
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.TextureGallery;
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.util.Grid;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.World;
import lombok.Getter;
@ -79,6 +80,23 @@ public void render(World world, Vector2i tile, TileMetaConsumer tileMetaConsumer
TileModel.instancePool().recycleInstance(model);
}
/**
* Un-renders a tile.
* The hires tile is deleted and the tileMetaConsumer (lowres) is updated with default values in the tiles area.
*/
public void unrender(Vector2i tile, TileMetaConsumer tileMetaConsumer) {
try {
storage.delete(tile.getX(), tile.getY());
} catch (IOException ex) {
Logger.global.logError("Failed to delete hires model: " + tile, ex);
}
Color color = new Color();
tileGrid.forEachIntersecting(tile, Grid.UNIT, (x, z) ->
tileMetaConsumer.set(x, z, color, 0, 0)
);
}
private void save(final TileModel model, Vector2i tile) {
try (
OutputStream out = storage.write(tile.getX(), tile.getY());

View File

@ -28,7 +28,7 @@
import de.bluecolored.bluemap.core.map.TextureGallery;
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.map.hires.blockmodel.BlockStateModelFactory;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
@ -65,8 +65,8 @@ public void render(World world, Vector3i modelMin, Vector3i modelMax, TileModel
BlockModelView blockModel = new BlockModelView(model);
int x, y, z;
for (x = min.getX(); x <= max.getX(); x++){
for (z = min.getZ(); z <= max.getZ(); z++){
for (x = modelMin.getX(); x <= modelMax.getX(); x++){
for (z = modelMin.getZ(); z <= modelMax.getZ(); z++){
maxHeight = 0;
topBlockLight = 0;

View File

@ -105,20 +105,22 @@ default boolean isInsideRenderBoundaries(int x, int y, int z) {
y <= max.getY();
}
/**
* Returns a predicate which is filtering out all cells of a {@link Grid}
* that are completely outside the render boundaries.
*/
default Predicate<Vector2i> getRenderBoundariesCellFilter(Grid grid) {
return t -> {
Vector2i cellMin = grid.getCellMin(t);
default boolean isInsideRenderBoundaries(Vector2i cell, Grid grid, boolean allowPartiallyIncludedCells) {
Vector2i cellMin = allowPartiallyIncludedCells ? grid.getCellMin(cell) : grid.getCellMax(cell);
if (cellMin.getX() > getMaxPos().getX()) return false;
if (cellMin.getY() > getMaxPos().getZ()) return false;
Vector2i cellMax = grid.getCellMax(t);
Vector2i cellMax = allowPartiallyIncludedCells ? grid.getCellMax(cell) : grid.getCellMin(cell);
if (cellMax.getX() < getMinPos().getX()) return false;
return cellMax.getY() >= getMinPos().getZ();
};
}
/**
* Returns a predicate which is filtering out all cells of a {@link Grid}
* that are outside the render boundaries.
*/
default Predicate<Vector2i> getCellRenderBoundariesFilter(Grid grid, boolean allowPartiallyIncludedCells) {
return cell -> isInsideRenderBoundaries(cell, grid, allowPartiallyIncludedCells);
}
boolean isSaveHiresLayer();

View File

@ -27,9 +27,9 @@
import de.bluecolored.bluemap.core.map.TextureGallery;
import de.bluecolored.bluemap.core.map.hires.BlockModelView;
import de.bluecolored.bluemap.core.map.hires.RenderSettings;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import de.bluecolored.bluemap.core.world.BlockState;

View File

@ -32,11 +32,11 @@
import de.bluecolored.bluemap.core.map.hires.RenderSettings;
import de.bluecolored.bluemap.core.resources.BlockColorCalculatorFactory;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.TextureVariable;
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.TextureVariable;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.util.math.MatrixM3f;
@ -138,7 +138,7 @@ private void build() {
tintcolor.set(1f, 1f, 1f, 1f, true);
if (blockState.isWater()) {
blockColorCalculator.getWaterAverageColor(block, tintcolor);
blockColorCalculator.getBlendedWaterColor(block, tintcolor);
}
int modelStart = blockModel.getStart();

View File

@ -34,12 +34,12 @@
import de.bluecolored.bluemap.core.map.hires.RenderSettings;
import de.bluecolored.bluemap.core.resources.BlockColorCalculatorFactory;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.Element;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.Face;
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.Element;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.Face;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate.Variant;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.util.Direction;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.util.math.MatrixM4f;

View File

@ -0,0 +1,116 @@
/*
* 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.map.renderstate;
import com.flowpowered.math.vector.Vector2i;
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.PalettedArrayAdapter;
import de.bluecolored.bluemap.core.util.RegistryAdapter;
import de.bluecolored.bluenbt.BlueNBT;
import lombok.Getter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.LinkedHashMap;
import java.util.Map;
@DebugDump
abstract class CellStorage<T extends CellStorage.Cell> {
private static final BlueNBT BLUE_NBT = new BlueNBT();
static {
BLUE_NBT.register(TypeToken.get(TileState.class), new RegistryAdapter<>(TileState.REGISTRY, Key.BLUEMAP_NAMESPACE, TileState.UNKNOWN));
BLUE_NBT.register(TypeToken.get(TileState[].class), new PalettedArrayAdapter<>(BLUE_NBT, TileState.class));
}
private static final int CACHE_SIZE = 4;
@Getter private final GridStorage storage;
private final Class<T> type;
private final LinkedHashMap<Vector2i, T> cells = new LinkedHashMap<>(
8,
0.75f,
true
) {
@Override
protected boolean removeEldestEntry(Map.Entry<Vector2i, T> eldest) {
if (this.size() <= CACHE_SIZE) return false;
saveCell(eldest.getKey(), eldest.getValue());
return true;
}
};
public CellStorage(GridStorage storage, Class<T> type) {
this.storage = storage;
this.type = type;
}
public synchronized void save() {
cells.forEach(this::saveCell);
}
public synchronized void reset() {
cells.clear();
}
T cell(int x, int z) {
return cell(new Vector2i(x, z));
}
synchronized T cell(Vector2i pos) {
return cells.computeIfAbsent(pos, this::loadCell);
}
private synchronized T loadCell(Vector2i pos) {
try (CompressedInputStream in = storage.read(pos.getX(), pos.getY())) {
if (in != null)
return BLUE_NBT.read(in.decompress(), type);
} catch (IOException ex) {
Logger.global.logError("Failed to load render-state cell " + pos, ex);
}
return createNewCell();
}
protected abstract T createNewCell();
private synchronized void saveCell(Vector2i pos, T cell) {
if (!cell.isModified()) return;
try (OutputStream in = storage.write(pos.getX(), pos.getY())) {
BLUE_NBT.write(cell, in, type);
} catch (IOException ex) {
Logger.global.logError("Failed to save render-state cell " + pos, ex);
}
}
public interface Cell {
boolean isModified();
}
}

View File

@ -0,0 +1,55 @@
package de.bluecolored.bluemap.core.map.renderstate;
import de.bluecolored.bluenbt.NBTName;
import de.bluecolored.bluenbt.NBTPostDeserialize;
import lombok.Getter;
import static de.bluecolored.bluemap.core.map.renderstate.MapChunkState.SHIFT;
public class ChunkInfoRegion implements CellStorage.Cell {
static final int REGION_LENGTH = 1 << SHIFT;
static final int REGION_MASK = REGION_LENGTH - 1;
static final int CHUNKS_PER_REGION = REGION_LENGTH * REGION_LENGTH;
@NBTName("chunk-hashes")
private int[] chunkHashes;
@Getter
private transient boolean modified;
private ChunkInfoRegion() {}
@NBTPostDeserialize
public void init() {
if (chunkHashes == null || chunkHashes.length != CHUNKS_PER_REGION)
chunkHashes = new int[CHUNKS_PER_REGION];
}
public int get(int x, int z) {
return chunkHashes[index(x, z)];
}
public int set(int x, int z, int hash) {
int index = index(x, z);
int previous = chunkHashes[index];
chunkHashes[index] = hash;
if (previous != hash)
modified = true;
return previous;
}
private static int index(int x, int z) {
return (z & REGION_MASK) << SHIFT | (x & REGION_MASK);
}
public static ChunkInfoRegion create() {
ChunkInfoRegion region = new ChunkInfoRegion();
region.init();
return region;
}
}

View File

@ -22,28 +22,31 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.biome.datapack;
package de.bluecolored.bluemap.core.map.renderstate;
import de.bluecolored.bluemap.core.world.Biome;
import lombok.Getter;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.storage.GridStorage;
@SuppressWarnings("FieldMayBeFinal")
@Getter
public class DpBiome {
@DebugDump
public class MapChunkState extends CellStorage<ChunkInfoRegion> {
private DpBiomeEffects effects = new DpBiomeEffects();
private float temperature = Biome.DEFAULT.getTemp();
private float downfall = Biome.DEFAULT.getHumidity();
static final int SHIFT = 7;
public Biome createBiome(String formatted) {
return new Biome(
formatted,
downfall,
temperature,
effects.getWaterColor(),
effects.getFoliageColor(),
effects.getGrassColor()
);
public MapChunkState(GridStorage storage) {
super(storage, ChunkInfoRegion.class);
}
public int get(int x, int z) {
return cell(x >> SHIFT, z >> SHIFT).get(x, z);
}
public synchronized int set(int x, int z, int hash) {
return cell(x >> SHIFT, z >> SHIFT).set(x, z, hash);
}
@Override
protected ChunkInfoRegion createNewCell() {
return ChunkInfoRegion.create();
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.map.renderstate;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.util.Grid;
import lombok.Getter;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
@DebugDump
public class MapTileState extends CellStorage<TileInfoRegion> {
static final int SHIFT = 5;
public static final Grid GRID = new Grid(1 << SHIFT);
@Getter private int lastRenderTime = -1;
private final Map<TileState, Integer> chunkStateCounts = new ConcurrentHashMap<>();
public MapTileState(GridStorage storage) {
super(storage, TileInfoRegion.class);
}
public synchronized void load() {
lastRenderTime = -1;
chunkStateCounts.clear();
try (Stream<GridStorage.Cell> stream = getStorage().stream()) {
stream.forEach(cell -> {
TileInfoRegion region = cell(cell.getX(), cell.getZ());
lastRenderTime = Math.max(lastRenderTime, region.findLatestRenderTime());
region.populateSummaryMap(chunkStateCounts);
});
} catch (IOException e) {
Logger.global.logError("Failed to load render-state regions", e);
}
}
public synchronized void reset() {
super.reset();
load();
}
public TileInfoRegion.TileInfo get(int x, int z) {
return cell(x >> SHIFT, z >> SHIFT).get(x, z);
}
public synchronized TileInfoRegion.TileInfo set(int x, int z, TileInfoRegion.TileInfo info) {
TileInfoRegion.TileInfo old = cell(x >> SHIFT, z >> SHIFT).set(x, z, info);
if (info.getRenderTime() > lastRenderTime)
lastRenderTime = info.getRenderTime();
if (old.getState() != info.getState()) {
chunkStateCounts.merge(old.getState(), -1, Integer::sum);
chunkStateCounts.merge(info.getState(), 1, Integer::sum);
}
return old;
}
public Map<TileState, Integer> getChunkStateCounts() {
return Collections.unmodifiableMap(chunkStateCounts);
}
@Override
protected synchronized TileInfoRegion createNewCell() {
TileInfoRegion region = TileInfoRegion.create();
region.populateSummaryMap(chunkStateCounts);
return region;
}
}

View File

@ -0,0 +1,42 @@
package de.bluecolored.bluemap.core.map.renderstate;
import java.util.Objects;
import static de.bluecolored.bluemap.core.map.renderstate.TileState.*;
@FunctionalInterface
public interface TileActionResolver {
ActionAndNextState findActionAndNextState(
boolean chunksChanged,
BoundsSituation bounds
);
enum BoundsSituation {
INSIDE,
EDGE,
OUTSIDE
}
enum Action {
NONE,
RENDER,
DELETE
}
record ActionAndNextState (Action action, TileState state) {
public ActionAndNextState(Action action, TileState state) {
this.action = Objects.requireNonNull(action);
this.state = Objects.requireNonNull(state);
}
public static final ActionAndNextState RENDER_RENDERED = new ActionAndNextState(Action.RENDER, RENDERED);
public static final ActionAndNextState NONE_RENDERED = new ActionAndNextState(Action.NONE, RENDERED);
public static final ActionAndNextState RENDER_RENDERED_EDGE = new ActionAndNextState(Action.RENDER, RENDERED_EDGE);
public static final ActionAndNextState NONE_RENDERED_EDGE = new ActionAndNextState(Action.NONE, RENDERED_EDGE);
public static final ActionAndNextState DELETE_OUT_OF_BOUNDS = new ActionAndNextState(Action.DELETE, OUT_OF_BOUNDS);
public static final ActionAndNextState NONE_OUT_OF_BOUNDS = new ActionAndNextState(Action.NONE, OUT_OF_BOUNDS);
}
}

View File

@ -0,0 +1,101 @@
package de.bluecolored.bluemap.core.map.renderstate;
import de.bluecolored.bluenbt.NBTName;
import de.bluecolored.bluenbt.NBTPostDeserialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import static de.bluecolored.bluemap.core.map.renderstate.MapTileState.SHIFT;
public class TileInfoRegion implements CellStorage.Cell {
private static final int REGION_LENGTH = 1 << SHIFT;
private static final int REGION_MASK = REGION_LENGTH - 1;
private static final int TILES_PER_REGION = REGION_LENGTH * REGION_LENGTH;
@NBTName("last-render-times")
private int[] lastRenderTimes;
@NBTName("tile-states")
private TileState[] tileStates;
@Getter
private transient boolean modified;
private TileInfoRegion() {}
@NBTPostDeserialize
public void init() {
if (lastRenderTimes == null || lastRenderTimes.length != TILES_PER_REGION)
lastRenderTimes = new int[TILES_PER_REGION];
if (tileStates == null || tileStates.length != TILES_PER_REGION) {
tileStates = new TileState[TILES_PER_REGION];
Arrays.fill(tileStates, TileState.UNKNOWN);
}
}
public TileInfo get(int x, int z) {
int index = index(x, z);
return new TileInfo(
lastRenderTimes[index],
tileStates[index]
);
}
public TileInfo set(int x, int z, TileInfo info) {
int index = index(x, z);
TileInfo previous = new TileInfo(
lastRenderTimes[index],
tileStates[index]
);
lastRenderTimes[index] = info.getRenderTime();
tileStates[index] = Objects.requireNonNull(info.getState());
if (!previous.equals(info))
this.modified = true;
return previous;
}
int findLatestRenderTime() {
if (lastRenderTimes == null) return -1;
return Arrays.stream(lastRenderTimes)
.max()
.orElse(-1);
}
void populateSummaryMap(Map<TileState, Integer> map) {
for (int i = 0; i < TILES_PER_REGION; i++) {
TileState tileState = tileStates[i];
map.merge(tileState, 1, Integer::sum);
}
}
private static int index(int x, int z) {
return (z & REGION_MASK) << SHIFT | (x & REGION_MASK);
}
@Data
@AllArgsConstructor
public static class TileInfo {
private int renderTime;
private TileState state;
}
public static TileInfoRegion create() {
TileInfoRegion region = new TileInfoRegion();
region.init();
return region;
}
}

View File

@ -0,0 +1,102 @@
package de.bluecolored.bluemap.core.map.renderstate;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import static de.bluecolored.bluemap.core.map.renderstate.TileActionResolver.ActionAndNextState.*;
public interface TileState extends Keyed, TileActionResolver {
TileState UNKNOWN = new Impl( Key.bluemap("unknown"));
TileState RENDERED = new Impl(Key.bluemap("rendered"), (changed, bounds) ->
switch (bounds) {
case INSIDE -> changed ? RENDER_RENDERED : NONE_RENDERED;
case EDGE -> RENDER_RENDERED_EDGE;
case OUTSIDE -> DELETE_OUT_OF_BOUNDS;
}
);
TileState RENDERED_EDGE = new Impl(Key.bluemap("rendered-edge"), (changed, bounds) ->
switch (bounds) {
case INSIDE -> RENDER_RENDERED;
case EDGE -> changed ? RENDER_RENDERED_EDGE : NONE_RENDERED_EDGE;
case OUTSIDE -> DELETE_OUT_OF_BOUNDS;
}
);
TileState OUT_OF_BOUNDS = new Impl(Key.bluemap("out-of-bounds"), (changed, bounds) ->
switch (bounds) {
case INSIDE -> RENDER_RENDERED;
case EDGE -> RENDER_RENDERED_EDGE;
case OUTSIDE -> NONE_OUT_OF_BOUNDS;
}
);
TileState NOT_GENERATED = new Impl(Key.bluemap("not-generated"));
TileState MISSING_LIGHT = new Impl(Key.bluemap("missing-light"));
TileState LOW_INHABITED_TIME = new Impl(Key.bluemap("low-inhabited-time"));
TileState CHUNK_ERROR = new Impl(Key.bluemap("chunk-error"));
TileState RENDER_ERROR = new Impl(Key.bluemap("render-error"), (changed, bounds) ->
switch (bounds) {
case INSIDE -> RENDER_RENDERED;
case EDGE -> RENDER_RENDERED_EDGE;
case OUTSIDE -> DELETE_OUT_OF_BOUNDS;
}
);
Registry<TileState> REGISTRY = new Registry<>(
UNKNOWN,
RENDERED,
RENDERED_EDGE,
OUT_OF_BOUNDS,
NOT_GENERATED,
MISSING_LIGHT,
LOW_INHABITED_TIME,
CHUNK_ERROR,
RENDER_ERROR
);
@Getter
@RequiredArgsConstructor
class Impl implements TileState {
private final Key key;
private final TileActionResolver resolver;
public Impl(Key key) {
this.key = key;
this.resolver = (changed, bounds) -> {
if (!changed) return noActionThisNextState();
return switch (bounds) {
case INSIDE -> RENDER_RENDERED;
case EDGE -> RENDER_RENDERED_EDGE;
case OUTSIDE -> DELETE_OUT_OF_BOUNDS;
};
};
}
@Override
public String toString() {
return key.getFormatted();
}
@Override
public ActionAndNextState findActionAndNextState(
boolean changed,
BoundsSituation bounds
) {
return resolver.findActionAndNextState(changed, bounds);
}
private ActionAndNextState noActionThisNextState;
private ActionAndNextState noActionThisNextState() {
if (noActionThisNextState == null)
noActionThisNextState = new ActionAndNextState(Action.NONE, this);
return noActionThisNextState;
}
}
}

View File

@ -29,6 +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.block.Block;
import de.bluecolored.bluemap.core.world.block.BlockNeighborhood;
import java.awt.image.BufferedImage;
@ -42,13 +43,15 @@
@DebugDump
public class BlockColorCalculatorFactory {
private static final int BLEND_RADIUS_H = 2;
private static final int BLEND_RADIUS_V = 1;
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;
BLEND_MIN_X = - BLEND_RADIUS_H,
BLEND_MAX_X = BLEND_RADIUS_H,
BLEND_MIN_Y = - BLEND_RADIUS_V,
BLEND_MAX_Y = BLEND_RADIUS_V,
BLEND_MIN_Z = - BLEND_RADIUS_H,
BLEND_MAX_Z = BLEND_RADIUS_H;
private final int[] foliageMap = new int[65536];
private final int[] grassMap = new int[65536];
@ -73,13 +76,13 @@ public void load(Path configFile) throws IOException {
ColorFunction colorFunction;
switch (value) {
case "@foliage":
colorFunction = BlockColorCalculator::getFoliageAverageColor;
colorFunction = BlockColorCalculator::getBlendedFoliageColor;
break;
case "@grass":
colorFunction = BlockColorCalculator::getGrassAverageColor;
colorFunction = BlockColorCalculator::getBlendedGrassColor;
break;
case "@water":
colorFunction = BlockColorCalculator::getWaterAverageColor;
colorFunction = BlockColorCalculator::getBlendedWaterColor;
break;
case "@redstone":
colorFunction = BlockColorCalculator::getRedstoneColor;
@ -120,17 +123,18 @@ public class BlockColorCalculator {
private final Color tempColor = new Color();
@SuppressWarnings("UnusedReturnValue")
public Color getBlockColor(BlockNeighborhood<?> block, Color target) {
String blockId = block.getBlockState().getFormatted();
ColorFunction colorFunction = blockColorMap.get(blockId);
if (colorFunction == null) colorFunction = blockColorMap.get("default");
if (colorFunction == null) colorFunction = BlockColorCalculator::getFoliageAverageColor;
if (colorFunction == null) colorFunction = BlockColorCalculator::getBlendedFoliageColor;
return colorFunction.invoke(this, block, target);
}
public Color getRedstoneColor(BlockNeighborhood<?> block, Color target) {
public Color getRedstoneColor(Block<?> block, Color target) {
int power = block.getBlockState().getRedstonePower();
return target.set(
(power + 5f) / 20f, 0f, 0f,
@ -138,15 +142,15 @@ public Color getRedstoneColor(BlockNeighborhood<?> block, Color target) {
);
}
public Color getWaterAverageColor(BlockNeighborhood<?> block, Color target) {
public Color getBlendedWaterColor(BlockNeighborhood<?> block, Color target) {
target.set(0, 0, 0, 0, true);
int x, y, z;
Biome biome;
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++) {
for (y = BLEND_MIN_Y; y <= BLEND_MAX_Y; y++) {
for (x = BLEND_MIN_X; x <= BLEND_MAX_X; x++) {
for (z = BLEND_MIN_Z; z <= BLEND_MAX_Z; z++) {
biome = block.getNeighborBlock(x, y, z).getBiome();
target.add(biome.getWaterColor());
}
@ -156,15 +160,15 @@ public Color getWaterAverageColor(BlockNeighborhood<?> block, Color target) {
return target.flatten();
}
public Color getFoliageAverageColor(BlockNeighborhood<?> block, Color target) {
public Color getBlendedFoliageColor(BlockNeighborhood<?> block, Color target) {
target.set(0, 0, 0, 0, true);
int x, y, z;
Biome biome;
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++) {
for (y = BLEND_MIN_Y; y <= BLEND_MAX_Y; y++) {
for (x = BLEND_MIN_X; x <= BLEND_MAX_X; x++) {
for (z = BLEND_MIN_Z; z <= BLEND_MAX_Z; z++) {
biome = block.getNeighborBlock(x, y, z).getBiome();
target.add(getFoliageColor(biome, tempColor));
}
@ -179,15 +183,15 @@ public Color getFoliageColor(Biome biome, Color target) {
return target.overlay(biome.getOverlayFoliageColor());
}
public Color getGrassAverageColor(BlockNeighborhood<?> block, Color target) {
public Color getBlendedGrassColor(BlockNeighborhood<?> block, Color target) {
target.set(0, 0, 0, 0, true);
int x, y, z;
Biome biome;
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++) {
for (y = BLEND_MIN_Y; y <= BLEND_MAX_Y; y++) {
for (x = BLEND_MIN_X; x <= BLEND_MAX_X; x++) {
for (z = BLEND_MIN_Z; z <= BLEND_MAX_Z; z++) {
biome = block.getNeighborBlock(x, y, z).getBiome();
target.add(getGrassColor(biome, tempColor));
}
@ -203,13 +207,13 @@ public Color getGrassColor(Biome biome, Color target) {
}
private void getColorFromMap(Biome biome, int[] colorMap, int defaultColor, Color target) {
double temperature = GenericMath.clamp(biome.getTemp(), 0.0, 1.0);
double humidity = GenericMath.clamp(biome.getHumidity(), 0.0, 1.0);
double temperature = GenericMath.clamp(biome.getTemperature(), 0.0, 1.0);
double downfall = GenericMath.clamp(biome.getDownfall(), 0.0, 1.0);
humidity *= temperature;
downfall *= temperature;
int x = (int) ((1.0 - temperature) * 255.0);
int y = (int) ((1.0 - humidity) * 255.0);
int y = (int) ((1.0 - downfall) * 255.0);
int index = y << 8 | x;
int color = (index >= colorMap.length ? defaultColor : colorMap[index]) | 0xFF000000;

View File

@ -0,0 +1,245 @@
/*
* 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.resources;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.FileHelper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.net.URL;
import java.nio.file.*;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@DebugDump
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class MinecraftVersion {
private static final Gson GSON = new Gson();
private static final String LATEST_KNOWN_VERSION = "1.20.6";
private static final String EARLIEST_RESOURCEPACK_VERSION = "1.13";
private static final String EARLIEST_DATAPACK_VERSION = "1.19.4";
private final String id;
private final Path resourcePack;
private final int resourcePackVersion;
private final Path dataPack;
private final int dataPackVersion;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MinecraftVersion that = (MinecraftVersion) o;
return id.equals(that.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
public static MinecraftVersion load(@Nullable String id, Path dataRoot, boolean allowDownload) throws IOException {
Path resourcePack;
Path dataPack;
try {
VersionManifest manifest = VersionManifest.getOrFetch();
if (id == null) id = manifest.getLatest().getRelease();
VersionManifest.Version version = manifest.getVersion(id);
VersionManifest.Version resourcePackVersion = manifest.getVersion(EARLIEST_RESOURCEPACK_VERSION);
VersionManifest.Version dataPackVersion = manifest.getVersion(EARLIEST_DATAPACK_VERSION);
if (version == null) {
Logger.global.logWarning("Could not find any version for id '" + id + "'. Using fallback-version: " + LATEST_KNOWN_VERSION);
version = manifest.getVersion(LATEST_KNOWN_VERSION);
}
if (version == null || resourcePackVersion == null || dataPackVersion == null) {
throw new IOException("Manifest is missing versions.");
}
if (version.compareTo(resourcePackVersion) > 0) resourcePackVersion = version;
if (version.compareTo(dataPackVersion) > 0) dataPackVersion = version;
resourcePack = dataRoot.resolve(getClientVersionFileName(resourcePackVersion.getId()));
dataPack = dataRoot.resolve(getClientVersionFileName(dataPackVersion.getId()));
if (allowDownload) {
if (!Files.exists(resourcePack)) download(resourcePackVersion, resourcePack);
if (!Files.exists(dataPack)) download(dataPackVersion, dataPack);
}
} catch (IOException ex) {
if (id == null) throw ex;
Logger.global.logWarning("Failed to fetch version-info from mojang-servers: " + ex);
resourcePack = dataRoot.resolve(getClientVersionFileName(id));
dataPack = resourcePack;
}
if (!Files.exists(resourcePack)) throw new IOException("Resource-File missing: " + resourcePack);
if (!Files.exists(dataPack)) throw new IOException("Resource-File missing: " + dataPack);
VersionInfo resourcePackVersionInfo = loadVersionInfo(resourcePack);
VersionInfo dataPackVersionInfo = resourcePack.equals(dataPack) ? resourcePackVersionInfo : loadVersionInfo(dataPack);
return new MinecraftVersion(
id,
resourcePack, resourcePackVersionInfo.getResourcePackVersion(),
dataPack, dataPackVersionInfo.getDataPackVersion()
);
}
private static void download(VersionManifest.Version version, Path file) throws IOException {
boolean downloadCompletedAndVerified = false;
VersionManifest.Download download = version.fetchDetail().getDownloads().getClient();
Logger.global.logInfo("Downloading '" + download.getUrl() + "' to '" + file + "'...");
FileHelper.createDirectories(file.toAbsolutePath().normalize().getParent());
try (
DigestInputStream in = new DigestInputStream(new URL(download.getUrl()).openStream(), MessageDigest.getInstance("SHA-1"));
OutputStream out = Files.newOutputStream(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.TRUNCATE_EXISTING)
) {
in.transferTo(out);
// verify sha-1
if (!Arrays.equals(
in.getMessageDigest().digest(),
hexStringToByteArray(download.getSha1())
)) {
throw new IOException("SHA-1 of the downloaded file does not match!");
}
downloadCompletedAndVerified = true;
} catch (NoSuchAlgorithmException | IOException ex) {
Logger.global.logWarning("Failed to download '" + download.getUrl() + "': " + ex);
} finally {
if (!downloadCompletedAndVerified)
Files.deleteIfExists(file);
}
}
private static String getClientVersionFileName(String versionId) {
return "minecraft-client-" + versionId + ".jar";
}
public static byte[] hexStringToByteArray(String hexString) {
int length = hexString.length();
if (length % 2 != 0)
throw new IllegalArgumentException("Invalid hex-string.");
int halfLength = length / 2;
byte[] data = new byte[halfLength];
int c;
for (int i = 0; i < halfLength; i += 1) {
c = i * 2;
data[i] = (byte) (
(Character.digit(hexString.charAt(c), 16) << 4) +
Character.digit(hexString.charAt(c + 1), 16)
);
}
return data;
}
private static VersionInfo loadVersionInfo(Path file) throws IOException {
try (FileSystem fileSystem = FileSystems.newFileSystem(file, (ClassLoader) null)) {
for (Path fsRoot : fileSystem.getRootDirectories()) {
if (!Files.isDirectory(fsRoot)) continue;
Path versionFile = fsRoot.resolve("version.json");
if (!Files.exists(versionFile)) continue;
try (Reader reader = Files.newBufferedReader(file)) {
return GSON.fromJson(reader, VersionInfo.class);
}
}
throw new IOException("'" + file + "' does not contain a 'version.json'");
}
}
@Getter
@RequiredArgsConstructor
@JsonAdapter(VersionInfoAdapter.class)
public static class VersionInfo {
private final int resourcePackVersion;
private final int dataPackVersion;
}
public static class VersionInfoAdapter extends AbstractTypeAdapterFactory<VersionInfo> {
public VersionInfoAdapter(Class<VersionInfo> type) {
super(type);
}
@Override
public VersionInfo read(JsonReader in, Gson gson) throws IOException {
JsonObject object = gson.fromJson(in, JsonObject.class);
JsonElement packVersion = object.get("pack_version");
if (packVersion instanceof JsonObject packVersionObject) {
return new VersionInfo(
packVersionObject.get("resource").getAsInt(),
packVersionObject.get("data").getAsInt()
);
} else {
int version = packVersion.getAsInt();
return new VersionInfo(
version,
version
);
}
}
}
}

View File

@ -0,0 +1,86 @@
package de.bluecolored.bluemap.core.resources;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@Getter
@SuppressWarnings({"FieldMayBeFinal", "unused"})
public class PackMeta {
private Pack pack = new Pack();
private Overlays overlays = new Overlays();
@Getter
public static class Pack {
private VersionRange packFormat = new VersionRange();
private @Nullable VersionRange supportedFormats;
}
@Getter
public static class Overlays {
private Overlay[] entries = new Overlay[0];
}
@Getter
public static class Overlay {
private VersionRange formats = new VersionRange();
private @Nullable String directory;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@JsonAdapter(VersionRange.Adapter.class)
public static class VersionRange {
private int minInclusive = Integer.MIN_VALUE;
private int maxInclusive = Integer.MAX_VALUE;
public boolean includes(int version) {
return version >= minInclusive && version <= maxInclusive;
}
private static class Adapter extends AbstractTypeAdapterFactory<VersionRange> {
public Adapter(Class<VersionRange> type) {
super(type);
}
@Override
public VersionRange read(JsonReader in, Gson gson) throws IOException {
return switch (in.peek()) {
case NUMBER -> {
int version = in.nextInt();
yield new VersionRange(version, version);
}
case BEGIN_ARRAY -> {
in.beginArray();
VersionRange range = new VersionRange(
in.nextInt(),
in.nextInt()
);
while (in.peek() != JsonToken.END_ARRAY)
in.skipValue();
in.endArray();
yield range;
}
default -> gson
.getDelegateAdapter(this, TypeToken.get(VersionRange.class))
.read(in);
};
}
}
}
}

View File

@ -0,0 +1,133 @@
package de.bluecolored.bluemap.core.resources;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.bluecolored.bluemap.core.resources.adapter.LocalDateTimeAdapter;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@Getter
@SuppressWarnings({"FieldMayBeFinal", "unused"})
public class VersionManifest {
public static final String DOMAIN = "https://piston-meta.mojang.com/";
public static final String MANIFEST_URL = DOMAIN + "mc/game/version_manifest.json";
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
.create();
private static VersionManifest instance;
private Latest latest;
private Version[] versions;
@Getter(AccessLevel.NONE)
private transient @Nullable Map<String, Version> versionMap;
@Getter(AccessLevel.NONE)
private transient boolean sorted;
public static VersionManifest getOrFetch() throws IOException {
if (instance == null) return fetch();
return instance;
}
public static VersionManifest fetch() throws IOException {
try (
InputStream in = new URL(MANIFEST_URL).openStream();
Reader reader = new BufferedReader(new InputStreamReader(in))
) {
instance = GSON.fromJson(reader, VersionManifest.class);
}
return instance;
}
/**
* An array of versions, ordered newest first
*/
public synchronized Version[] getVersions() {
if (!sorted) Arrays.sort(versions, Comparator.reverseOrder());
return versions;
}
public synchronized @Nullable Version getVersion(String id) {
if (versionMap == null) {
versionMap = new HashMap<>();
for (Version version : versions)
versionMap.put(version.id, version);
}
return versionMap.get(id);
}
@Getter
public static class Latest {
private String release;
private String snapshot;
}
@Getter
public static class Version implements Comparable<Version> {
private String id;
private String type;
private String url;
private LocalDateTime time;
private LocalDateTime releaseTime;
@Getter(AccessLevel.NONE)
private transient @Nullable VersionDetail detail;
public synchronized VersionDetail fetchDetail() throws IOException {
if (detail == null) {
try (
InputStream in = new URL(url).openStream();
Reader reader = new BufferedReader(new InputStreamReader(in))
) {
detail = GSON.fromJson(reader, VersionDetail.class);
}
}
return detail;
}
@Override
public int compareTo(@NotNull VersionManifest.Version version) {
return releaseTime.compareTo(version.releaseTime);
}
}
@Getter
public static class VersionDetail {
private String id;
private String type;
private Downloads downloads;
}
@Getter
public static class Downloads {
private Download client;
private Download server;
}
@Getter
public static class Download {
private String url;
private long size;
private String sha1;
}
}

View File

@ -0,0 +1,23 @@
package de.bluecolored.bluemap.core.resources.adapter;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
@Override
public void write(JsonWriter out, LocalDateTime value) throws IOException {
out.value(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value));
}
@Override
public LocalDateTime read(JsonReader in) throws IOException {
return LocalDateTime.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(in.nextString()));
}
}

View File

@ -29,7 +29,7 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.Face;
import de.bluecolored.bluemap.core.resources.pack.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;

View File

@ -1,90 +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.resources.biome;
import com.google.gson.stream.JsonReader;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.biome.datapack.DpBiome;
import de.bluecolored.bluemap.core.world.Biome;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
@DebugDump
public class BiomeConfig {
private final Map<String, Biome> biomes;
public BiomeConfig() {
biomes = new HashMap<>();
}
public void load(Path configFile) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(configFile)) {
JsonReader json = new JsonReader(reader);
json.setLenient(true);
json.beginObject();
while (json.hasNext()) {
String formatted = json.nextName();
BiomeConfigEntry entry = ResourcesGson.INSTANCE.fromJson(json, BiomeConfigEntry.class);
Biome biome = entry.createBiome(formatted);
// don't overwrite already present values, higher priority resources are loaded first
biomes.putIfAbsent(biome.getFormatted(), biome);
}
json.endObject();
}
}
public void loadDatapackBiome(String namespace, Path biomeFile) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(biomeFile)) {
JsonReader json = new JsonReader(reader);
json.setLenient(true);
DpBiome dpBiome = ResourcesGson.INSTANCE.fromJson(json, DpBiome.class);
String formatted = namespace + ":" + biomeFile.getFileName().toString();
int fileEndingDot = formatted.lastIndexOf('.');
if (fileEndingDot != -1) formatted = formatted.substring(0, fileEndingDot);
Biome biome = dpBiome.createBiome(formatted);
// don't overwrite already present values, higher priority resources are loaded first
biomes.putIfAbsent(biome.getFormatted(), biome);
}
}
public Biome getBiome(String formatted) {
return biomes.getOrDefault(formatted, Biome.DEFAULT);
}
}

View File

@ -1,70 +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.resources.biome;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Biome;
@SuppressWarnings("FieldMayBeFinal")
public class BiomeConfigEntry {
private float humidity = Biome.DEFAULT.getHumidity();
private float temperature = Biome.DEFAULT.getTemp();
private Color watercolor = Biome.DEFAULT.getWaterColor();
private Color grasscolor = new Color();
private Color foliagecolor = new Color();
public Biome createBiome(String formatted) {
return new Biome(
formatted,
humidity,
temperature,
watercolor.premultiplied(),
foliagecolor.premultiplied(),
grasscolor.premultiplied()
);
}
public float getHumidity() {
return humidity;
}
public float getTemperature() {
return temperature;
}
public Color getWatercolor() {
return watercolor;
}
public Color getGrasscolor() {
return grasscolor;
}
public Color getFoliagecolor() {
return foliagecolor;
}
}

View File

@ -1,39 +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.resources.biome.datapack;
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 waterColor = Biome.DEFAULT.getWaterColor();
private Color foliageColor = Biome.DEFAULT.getOverlayFoliageColor();
private Color grassColor = Biome.DEFAULT.getOverlayGrassColor();
}

View File

@ -0,0 +1,125 @@
package de.bluecolored.bluemap.core.resources.pack;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resources.PackMeta;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
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.Map;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
@RequiredArgsConstructor
@Getter
public abstract class Pack {
private final int packVersion;
public abstract void loadResources(Iterable<Path> roots) throws IOException, InterruptedException;
protected void loadResourcePath(Path root, ResourcePack.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()) {
if (!Files.isDirectory(fsRoot)) continue;
loadResourcePath(fsRoot, resourceLoader);
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read '" + root + "': " + ex);
}
return;
}
// load nested jars from fabric.mod.json if present
Path fabricModJson = root.resolve("fabric.mod.json");
if (Files.isRegularFile(fabricModJson)) {
try (BufferedReader reader = Files.newBufferedReader(fabricModJson)) {
JsonObject rootElement = ResourcesGson.INSTANCE.fromJson(reader, JsonObject.class);
if (rootElement.has("jars")) {
for (JsonElement element : rootElement.getAsJsonArray("jars")) {
Path file = root.resolve(element.getAsJsonObject().get("file").getAsString());
if (Files.exists(file)) loadResourcePath(file, resourceLoader);
}
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read fabric.mod.json: " + ex);
}
}
// load overlays
Path packMetaFile = root.resolve("pack.mcmeta");
if (Files.isRegularFile(packMetaFile)) {
try (BufferedReader reader = Files.newBufferedReader(packMetaFile)) {
PackMeta packMeta = ResourcesGson.INSTANCE.fromJson(reader, PackMeta.class);
PackMeta.Overlay[] overlays = packMeta.getOverlays().getEntries();
for (int i = overlays.length - 1; i >= 0; i--) {
PackMeta.Overlay overlay = overlays[i];
String dir = overlay.getDirectory();
if (dir != null && overlay.getFormats().includes(this.packVersion)) {
Path overlayRoot = root.resolve(dir);
if (Files.exists(overlayRoot)) loadResourcePath(overlayRoot, resourceLoader);
}
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read pack.mcmeta: " + ex);
}
}
resourceLoader.load(root);
}
protected <T> void loadResource(Path root, Path file, Loader<T> loader, Map<? super ResourcePath<T>, 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(resourcePath);
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);
}
}
protected 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);
}
}
protected 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);
}
}
protected interface Loader<T> {
T load(ResourcePath<T> resourcePath) throws IOException;
}
protected interface PathLoader {
void load(Path root) throws IOException;
}
}

View File

@ -22,28 +22,27 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.datapack;
package de.bluecolored.bluemap.core.resources.pack.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.resources.pack.Pack;
import de.bluecolored.bluemap.core.resources.pack.datapack.biome.DatapackBiome;
import de.bluecolored.bluemap.core.resources.pack.datapack.dimension.DimensionTypeData;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.DimensionType;
import de.bluecolored.bluemap.core.world.mca.chunk.LegacyBiomes;
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 class DataPack extends Pack {
public static final Key DIMENSION_OVERWORLD = new Key("minecraft", "overworld");
public static final Key DIMENSION_THE_NETHER = new Key("minecraft", "the_nether");
@ -55,57 +54,57 @@ public class DataPack {
public static final Key DIMENSION_TYPE_THE_END = new Key("minecraft", "the_end");
private final Map<Key, DimensionType> dimensionTypes = new HashMap<>();
private final Map<Key, Biome> biomes = new HashMap<>();
@Nullable
public DimensionType getDimensionType(Key key) {
return dimensionTypes.get(key);
private LegacyBiomes legacyBiomes;
public DataPack(int packVersion) {
super(packVersion);
}
public void load(Path root) throws InterruptedException {
@Override
public void loadResources(Iterable<Path> roots) throws IOException, InterruptedException {
Logger.global.logInfo("Loading datapack...");
for (Path root : roots) {
Logger.global.logDebug("Loading datapack from: " + root + " ...");
loadPath(root);
loadResources(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;
Logger.global.logInfo("Baking datapack...");
bake();
Logger.global.logInfo("Datapack loaded.");
}
private void loadResources(Path root) throws InterruptedException, IOException {
loadResourcePath(root, this::loadPath);
}
private void loadPath(Path root) {
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, () -> {
.forEach(file -> loadResource(root, file, key -> {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return ResourcesGson.INSTANCE.fromJson(reader, DimensionTypeData.class);
}
}, dimensionTypes));
list(root.resolve("data"))
.map(path -> path.resolve("worldgen").resolve("biome"))
.filter(Files::isDirectory)
.flatMap(DataPack::walk)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.filter(Files::isRegularFile)
.forEach(file -> loadResource(root, file, key -> {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return new DatapackBiome(key, ResourcesGson.INSTANCE.fromJson(reader, DatapackBiome.Data.class));
}
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);
}
}, biomes));
}
public void bake() {
@ -113,29 +112,20 @@ public void bake() {
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);
legacyBiomes = new LegacyBiomes(this);
}
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);
}
public @Nullable DimensionType getDimensionType(Key key) {
return dimensionTypes.get(key);
}
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);
}
public @Nullable Biome getBiome(Key key) {
return biomes.get(key);
}
private interface Loader<T> {
T load() throws IOException;
public @Nullable Biome getBiome(int legacyId) {
return legacyBiomes.forId(legacyId);
}
}

View File

@ -0,0 +1,61 @@
package de.bluecolored.bluemap.core.resources.pack.datapack.biome;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Biome;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class DatapackBiome implements Biome {
private final Key key;
private final Data data;
@Override
public float getDownfall() {
return data.downfall;
}
@Override
public float getTemperature() {
return data.temperature;
}
@Override
public Color getWaterColor() {
return data.effects.waterColor;
}
@Override
public Color getOverlayFoliageColor() {
return data.effects.foliageColor;
}
@Override
public Color getOverlayGrassColor() {
return data.effects.grassColor;
}
@SuppressWarnings("FieldMayBeFinal")
@Getter
public static class Data {
private Effects effects = new Effects();
private float temperature = Biome.DEFAULT.getTemperature();
private float downfall = Biome.DEFAULT.getDownfall();
}
@SuppressWarnings("FieldMayBeFinal")
@Getter
public static class Effects {
private Color waterColor = Biome.DEFAULT.getWaterColor();
private Color foliageColor = Biome.DEFAULT.getOverlayFoliageColor();
private Color grassColor = Biome.DEFAULT.getOverlayGrassColor();
}
}

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.datapack.dimension;
package de.bluecolored.bluemap.core.resources.pack.datapack.dimension;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.world.DimensionType;

View File

@ -22,12 +22,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack;
package de.bluecolored.bluemap.core.resources.pack.resourcepack;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
@ -35,14 +33,13 @@
import de.bluecolored.bluemap.core.resources.BlockPropertiesConfig;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.adapter.ResourcesGson;
import de.bluecolored.bluemap.core.resources.biome.BiomeConfig;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.TextureVariable;
import de.bluecolored.bluemap.core.resources.resourcepack.blockstate.BlockState;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.AnimationMeta;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resources.pack.Pack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.TextureVariable;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate.BlockState;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.AnimationMeta;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.util.Tristate;
import de.bluecolored.bluemap.core.world.Biome;
import de.bluecolored.bluemap.core.world.BlockProperties;
import org.jetbrains.annotations.Nullable;
@ -53,8 +50,6 @@
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
@ -62,40 +57,36 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;
@DebugDump
public class ResourcePack {
public class ResourcePack extends Pack {
public static final ResourcePath<BlockState> MISSING_BLOCK_STATE = new ResourcePath<>("bluemap", "missing");
public static final ResourcePath<BlockModel> MISSING_BLOCK_MODEL = new ResourcePath<>("bluemap", "block/missing");
public static final ResourcePath<Texture> MISSING_TEXTURE = new ResourcePath<>("bluemap", "block/missing");
private final Map<String, ResourcePath<BlockState>> blockStatePaths;
private final Map<ResourcePath<BlockState>, BlockState> blockStates;
private final Map<String, ResourcePath<BlockModel>> blockModelPaths;
private final Map<ResourcePath<BlockModel>, BlockModel> blockModels;
private final Map<String, ResourcePath<Texture>> texturePaths;
private final Map<ResourcePath<Texture>, Texture> textures;
private final Map<ResourcePath<BufferedImage>, BufferedImage> colormaps;
private final Map<ResourcePath<BufferedImage>, BufferedImage> colormaps;
private final BlockColorCalculatorFactory colorCalculatorFactory;
private final BiomeConfig biomeConfig;
private final BlockPropertiesConfig blockPropertiesConfig;
private final Map<String, ResourcePath<BlockState>> blockStatePaths;
private final Map<String, ResourcePath<Texture>> texturePaths;
private final LoadingCache<de.bluecolored.bluemap.core.world.BlockState, BlockProperties> blockPropertiesCache;
public ResourcePack() {
public ResourcePack(int packVersion) {
super(packVersion);
this.blockStatePaths = new HashMap<>();
this.blockStates = new HashMap<>();
this.blockModelPaths = new HashMap<>();
this.blockModels = new HashMap<>();
this.texturePaths = new HashMap<>();
this.textures = new HashMap<>();
this.colormaps = new HashMap<>();
this.colorCalculatorFactory = new BlockColorCalculatorFactory();
this.biomeConfig = new BiomeConfig();
this.blockPropertiesConfig = new BlockPropertiesConfig();
this.blockPropertiesCache = Caffeine.newBuilder()
@ -104,88 +95,6 @@ public ResourcePack() {
.build(this::loadBlockProperties);
}
@Nullable
public ResourcePath<BlockState> getBlockStatePath(String formatted) {
return blockStatePaths.get(formatted);
}
@Nullable
public BlockState getBlockState(de.bluecolored.bluemap.core.world.BlockState blockState) {
ResourcePath<BlockState> path = blockStatePaths.get(blockState.getFormatted());
return path != null ? path.getResource(this::getBlockState) : MISSING_BLOCK_STATE.getResource(this::getBlockState);
}
@Nullable
public BlockState getBlockState(ResourcePath<BlockState> path) {
BlockState blockState = blockStates.get(path);
return blockState != null ? blockState : MISSING_BLOCK_STATE.getResource(blockStates::get);
}
public Map<ResourcePath<BlockState>, BlockState> getBlockStates() {
return blockStates;
}
@Nullable
public ResourcePath<BlockModel> getBlockModelPath(String formatted) {
return blockModelPaths.get(formatted);
}
@Nullable
public BlockModel getBlockModel(ResourcePath<BlockModel> path) {
BlockModel blockModel = blockModels.get(path);
return blockModel != null ? blockModel : MISSING_BLOCK_MODEL.getResource(blockModels::get);
}
public Map<ResourcePath<BlockModel>, BlockModel> getBlockModels() {
return blockModels;
}
@Nullable
public ResourcePath<Texture> getTexturePath(String formatted) {
return texturePaths.get(formatted);
}
@Nullable
public Texture getTexture(ResourcePath<Texture> path) {
Texture texture = textures.get(path);
return texture != null ? texture : MISSING_TEXTURE.getResource(textures::get);
}
public Map<ResourcePath<Texture>, Texture> getTextures() {
return textures;
}
public BlockColorCalculatorFactory getColorCalculatorFactory() {
return colorCalculatorFactory;
}
public Biome getBiome(String formatted) {
return biomeConfig.getBiome(formatted);
}
public BlockProperties getBlockProperties(de.bluecolored.bluemap.core.world.BlockState state) {
return blockPropertiesCache.get(state);
}
private BlockProperties loadBlockProperties(de.bluecolored.bluemap.core.world.BlockState state) {
BlockProperties.Builder props = blockPropertiesConfig.getBlockProperties(state).toBuilder();
if (props.isOccluding() == Tristate.UNDEFINED || props.isCulling() == Tristate.UNDEFINED) {
BlockState resource = getBlockState(state);
if (resource != null) {
resource.forEach(state,0, 0, 0, variant -> {
BlockModel model = variant.getModel().getResource(this::getBlockModel);
if (model != null) {
if (props.isOccluding() == Tristate.UNDEFINED) props.occluding(model.isOccluding());
if (props.isCulling() == Tristate.UNDEFINED) props.culling(model.isCulling());
}
});
}
}
return props.build();
}
public synchronized void loadResources(Iterable<Path> roots) throws IOException, InterruptedException {
Logger.global.logInfo("Loading resources...");
@ -209,43 +118,9 @@ public synchronized void loadResources(Iterable<Path> roots) throws IOException,
Logger.global.logInfo("Baking resources...");
bake();
Logger.global.logInfo("Resources loaded.");
}
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()) {
if (!Files.isDirectory(fsRoot)) continue;
loadResourcePath(fsRoot, resourceLoader);
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read '" + root + "': " + ex);
}
return;
}
// load nested jars from fabric.mod.json if present
Path fabricModJson = root.resolve("fabric.mod.json");
if (Files.isRegularFile(fabricModJson)) {
try (BufferedReader reader = Files.newBufferedReader(fabricModJson)) {
JsonObject rootElement = ResourcesGson.INSTANCE.fromJson(reader, JsonObject.class);
if (rootElement.has("jars")) {
for (JsonElement element : rootElement.getAsJsonArray("jars")) {
Path file = root.resolve(element.getAsJsonObject().get("file").getAsString());
if (Files.exists(file)) loadResourcePath(file, resourceLoader);
}
}
} catch (Exception ex) {
Logger.global.logDebug("Failed to read fabric.mod.json: " + ex);
}
}
resourceLoader.load(root);
}
private void loadResources(Path root) throws IOException {
try {
// do those in parallel
@ -259,7 +134,7 @@ private void loadResources(Path root) throws IOException {
.flatMap(ResourcePack::walk)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.filter(Files::isRegularFile)
.forEach(file -> loadResource(root, file, () -> {
.forEach(file -> loadResource(root, file, key -> {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return ResourcesGson.INSTANCE.fromJson(reader, BlockState.class);
}
@ -275,7 +150,7 @@ private void loadResources(Path root) throws IOException {
.flatMap(ResourcePack::walk)
.filter(path -> path.getFileName().toString().endsWith(".json"))
.filter(Files::isRegularFile)
.forEach(file -> loadResource(root, file, () -> {
.forEach(file -> loadResource(root, file, key -> {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return ResourcesGson.INSTANCE.fromJson(reader, BlockModel.class);
}
@ -287,7 +162,7 @@ private void loadResources(Path root) throws IOException {
walk(root.resolve("assets").resolve("minecraft").resolve("textures").resolve("colormap"))
.filter(path -> path.getFileName().toString().endsWith(".png"))
.filter(Files::isRegularFile)
.forEach(file -> loadResource(root, file, () -> {
.forEach(file -> loadResource(root, file, key -> {
try (InputStream in = Files.newInputStream(file)) {
return ImageIO.read(in);
}
@ -308,35 +183,6 @@ 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"))
.filter(Files::isRegularFile)
.forEach(file -> {
try {
biomeConfig.load(file);
} catch (Exception ex) {
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
}
});
list(root.resolve("data"))
.filter(Files::isDirectory)
.forEach(namespace -> list(namespace.resolve("worldgen").resolve("biome"))
.filter(path -> path.getFileName().toString().endsWith(".json"))
.filter(Files::isRegularFile)
.forEach(file -> {
try {
biomeConfig.loadDatapackBiome(namespace.getFileName().toString(), file);
} catch (Exception ex) {
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
}
})
);
}, BlueMap.THREAD_POOL),
// load block-properties configs
CompletableFuture.runAsync(() -> {
list(root.resolve("assets"))
@ -380,9 +226,8 @@ private void loadTextures(Path root) throws IOException {
.flatMap(ResourcePack::walk)
.filter(path -> path.getFileName().toString().endsWith(".png"))
.filter(Files::isRegularFile)
.forEach(file -> loadResource(root, file, () -> {
ResourcePath<Texture> resourcePath = new ResourcePath<>(root.relativize(file));
if (!usedTextures.contains(resourcePath)) return null; // don't load unused textures
.forEach(file -> loadResource(root, file, key -> {
if (!usedTextures.contains(key)) return null; // don't load unused textures
// load image
BufferedImage image;
@ -399,8 +244,7 @@ private void loadTextures(Path root) throws IOException {
}
}
return Texture.from(resourcePath, image, animation);
return Texture.from(key, image, animation);
}, textures));
} catch (RuntimeException ex) {
@ -415,7 +259,6 @@ private void bake() throws IOException, InterruptedException {
// fill path maps
blockStates.keySet().forEach(path -> blockStatePaths.put(path.getFormatted(), path));
blockModels.keySet().forEach(path -> blockModelPaths.put(path.getFormatted(), path));
textures.keySet().forEach(path -> texturePaths.put(path.getFormatted(), path));
// optimize references
@ -447,46 +290,64 @@ private void bake() throws IOException, InterruptedException {
}
private <T> void loadResource(Path root, Path file, Loader<T> loader, Map<ResourcePath<T>, T> resultMap) {
try {
ResourcePath<T> resourcePath = new ResourcePath<>(root.relativize(file));
if (resultMap.containsKey(resourcePath)) return; // don't load already present resources
@Nullable
public BlockState getBlockState(de.bluecolored.bluemap.core.world.BlockState blockState) {
ResourcePath<BlockState> path = blockStatePaths.get(blockState.getFormatted());
return path != null ? path.getResource(this::getBlockState) : MISSING_BLOCK_STATE.getResource(this::getBlockState);
}
T resource = loader.load();
if (resource == null) return; // don't load missing resources
@Nullable
public BlockState getBlockState(ResourcePath<BlockState> path) {
BlockState blockState = blockStates.get(path);
return blockState != null ? blockState : MISSING_BLOCK_STATE.getResource(blockStates::get);
}
resourcePath.setResource(resource);
resultMap.put(resourcePath, resource);
} catch (Exception ex) {
Logger.global.logDebug("Failed to parse resource-file '" + file + "': " + ex);
@Nullable
public BlockModel getBlockModel(ResourcePath<BlockModel> path) {
BlockModel blockModel = blockModels.get(path);
return blockModel != null ? blockModel : MISSING_BLOCK_MODEL.getResource(blockModels::get);
}
@Nullable
public ResourcePath<Texture> getTexturePath(String formatted) {
return texturePaths.get(formatted);
}
@Nullable
public Texture getTexture(ResourcePath<Texture> path) {
Texture texture = textures.get(path);
return texture != null ? texture : MISSING_TEXTURE.getResource(textures::get);
}
public Map<ResourcePath<Texture>, Texture> getTextures() {
return textures;
}
public BlockColorCalculatorFactory getColorCalculatorFactory() {
return colorCalculatorFactory;
}
public BlockProperties getBlockProperties(de.bluecolored.bluemap.core.world.BlockState state) {
return blockPropertiesCache.get(state);
}
private BlockProperties loadBlockProperties(de.bluecolored.bluemap.core.world.BlockState state) {
BlockProperties.Builder props = blockPropertiesConfig.getBlockProperties(state).toBuilder();
if (props.isOccluding() == Tristate.UNDEFINED || props.isCulling() == Tristate.UNDEFINED) {
BlockState resource = getBlockState(state);
if (resource != null) {
resource.forEach(state,0, 0, 0, variant -> {
BlockModel model = variant.getModel().getResource(this::getBlockModel);
if (model != null) {
if (props.isOccluding() == Tristate.UNDEFINED) props.occluding(model.isOccluding());
if (props.isCulling() == Tristate.UNDEFINED) props.culling(model.isCulling());
}
});
}
}
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;
}
private interface PathLoader {
void load(Path root) throws IOException;
return props.build();
}
}

View File

@ -22,12 +22,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockmodel;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.util.Direction;
import org.jetbrains.annotations.Nullable;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockmodel;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel;
import com.flowpowered.math.vector.Vector3f;
import com.flowpowered.math.vector.Vector4f;
@ -32,7 +32,7 @@
import com.google.gson.stream.JsonReader;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.Direction;
import java.io.IOException;

View File

@ -22,11 +22,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockmodel;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel;
import com.flowpowered.math.vector.Vector4f;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.Direction;
import java.util.function.Function;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockmodel;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel;
import com.flowpowered.math.TrigMath;
import com.flowpowered.math.vector.Vector3f;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockmodel;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
@ -30,8 +30,8 @@
import com.google.gson.stream.JsonWriter;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.texture.Texture;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.texture.Texture;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockstate;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate;
import de.bluecolored.bluemap.api.debug.DebugDump;
import org.jetbrains.annotations.Nullable;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockstate;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate;
import de.bluecolored.bluemap.api.debug.DebugDump;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockstate;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockstate;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
@ -31,8 +31,8 @@
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.AbstractTypeAdapterFactory;
import de.bluecolored.bluemap.core.resources.ResourcePath;
import de.bluecolored.bluemap.core.resources.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.resources.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.blockmodel.BlockModel;
import de.bluecolored.bluemap.core.resources.pack.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.math.MatrixM3f;
import java.io.IOException;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockstate;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.blockstate;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.blockstate;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.texture;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.texture;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.resources.resourcepack.texture;
package de.bluecolored.bluemap.core.resources.pack.resourcepack.texture;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.resources.ResourcePath;

View File

@ -62,6 +62,11 @@ public interface GridStorage {
*/
boolean exists(int x, int z) throws IOException;
/**
* Returns a {@link ItemStorage} for the given position
*/
ItemStorage cell(int x, int z);
/**
* Returns a stream over all <b>existing</b> items in this storage
*/
@ -72,7 +77,7 @@ public interface GridStorage {
*/
boolean isClosed();
interface Cell extends SingleItemStorage {
interface Cell extends ItemStorage {
/**
* Returns the x position of this item in the grid

View File

@ -30,7 +30,7 @@
import java.io.IOException;
import java.io.OutputStream;
public interface SingleItemStorage {
public interface ItemStorage {
/**
* Returns an {@link OutputStream} that can be used to write the item-data of this storage

View File

@ -0,0 +1,77 @@
package de.bluecolored.bluemap.core.storage;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.util.Key;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public abstract class KeyedMapStorage implements MapStorage {
private static final Key HIRES_TILES_KEY = Key.bluemap("hires");
private static final Key TILE_STATE_KEY = Key.bluemap("tile-state");
private static final Key CHUNK_STATE_KEY = Key.bluemap("chunk-state");
private static final Key SETTINGS_KEY = Key.bluemap("settings");
private static final Key TEXTURES_KEY = Key.bluemap("textures");
private static final Key MARKERS_KEY = Key.bluemap("markers");
private static final Key PLAYERS_KEY = Key.bluemap("players");
private final Compression compression;
@Override
public GridStorage hiresTiles() {
return grid(HIRES_TILES_KEY, compression);
}
@Override
public GridStorage lowresTiles(int lod) {
return grid(Key.bluemap("lowres/" + lod), Compression.NONE);
}
@Override
public GridStorage tileState() {
return grid(TILE_STATE_KEY, Compression.GZIP);
}
@Override
public GridStorage chunkState() {
return grid(CHUNK_STATE_KEY, Compression.GZIP);
}
@Override
public ItemStorage asset(String name) {
return item(Key.bluemap("asset/" + MapStorage.escapeAssetName(name)), Compression.NONE);
}
@Override
public ItemStorage settings() {
return item(SETTINGS_KEY, Compression.NONE);
}
@Override
public ItemStorage textures() {
return item(TEXTURES_KEY, compression);
}
@Override
public ItemStorage markers() {
return item(MARKERS_KEY, Compression.NONE);
}
@Override
public ItemStorage players() {
return item(PLAYERS_KEY, Compression.NONE);
}
/**
* Returns a {@link GridStorage} for the given {@link Key}.<br>
* The compressionHint can be used if a new {@link GridStorage} needs to be created, but is not guaranteed.
*/
public abstract GridStorage grid(Key key, Compression compressionHint);
/**
* Returns a {@link ItemStorage} for the given {@link Key}.<br>
* The compressionHint can be used if a new {@link ItemStorage} needs to be created, but is not guaranteed.
*/
public abstract ItemStorage item(Key key, Compression compressionHint);
}

View File

@ -40,34 +40,39 @@ public interface MapStorage {
GridStorage lowresTiles(int lod);
/**
* Returns a {@link SingleItemStorage} for a map asset with the given name
* Returns a {@link GridStorage} for the tile-state (meta-) data of this map
*/
SingleItemStorage asset(String name);
GridStorage tileState();
/**
* Returns a {@link SingleItemStorage} for the render-state data of this map
* Returns a {@link GridStorage} for the chunk-state (meta-) data of this map
*/
SingleItemStorage renderState();
GridStorage chunkState();
/**
* Returns a {@link SingleItemStorage} for the settings (settings.json) of this map
* Returns a {@link ItemStorage} for a map asset with the given name
*/
SingleItemStorage settings();
ItemStorage asset(String name);
/**
* Returns a {@link SingleItemStorage} for the texture-data (textures.json) of this map
* Returns a {@link ItemStorage} for the settings (settings.json) of this map
*/
SingleItemStorage textures();
ItemStorage settings();
/**
* Returns a {@link SingleItemStorage} for the marker-data (live/markers.json) of this map
* Returns a {@link ItemStorage} for the texture-data (textures.json) of this map
*/
SingleItemStorage markers();
ItemStorage textures();
/**
* Returns a {@link SingleItemStorage} for the player-data (live/players.json) of this map
* Returns a {@link ItemStorage} for the marker-data (live/markers.json) of this map
*/
SingleItemStorage players();
ItemStorage markers();
/**
* Returns a {@link ItemStorage} for the player-data (live/players.json) of this map
*/
ItemStorage players();
/**
* Deletes the entire map from the storage

View File

@ -37,7 +37,10 @@ public interface Storage extends Closeable {
void initialize() throws IOException;
/**
* Returns the {@link MapStorage} for the given mapId
* Returns the {@link MapStorage} for the given mapId.<br>
* <br>
* If this method is invoked multiple times with the same <code>mapId</code>, it is important that the returned MapStorage should at least
* be equal (<code>equals() == true</code>) to the previously returned storages!
*/
MapStorage map(String mapId);

View File

@ -36,6 +36,14 @@ public class CompressedInputStream extends DelegateInputStream {
private final Compression compression;
/**
* Creates a new CompressedInputStream with {@link Compression#NONE} from an (uncompressed) {@link InputStream}.
* This does <b>not</b> compress the provided InputStream.
*/
public CompressedInputStream(InputStream in) {
this(in, Compression.NONE);
}
/**
* Creates a new CompressedInputStream from an <b>already compressed</b> {@link InputStream} and the {@link Compression}
* it is compressed with.

View File

@ -24,15 +24,17 @@
*/
package de.bluecolored.bluemap.core.storage.file;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.ItemStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.Objects;
@ -41,68 +43,72 @@
import java.util.stream.Stream;
@RequiredArgsConstructor
class PathBasedGridStorage implements GridStorage {
class FileGridStorage implements GridStorage {
private static final Pattern ITEM_PATH_PATTERN = Pattern.compile("x(-?\\d+)z(-?\\d+)");
private final PathBasedMapStorage storage;
private final Path root;
private final String suffix;
private final Compression compression;
private final boolean atomic;
@Override
public OutputStream write(int x, int z) throws IOException {
return item(x, z).write();
return cell(x, z).write();
}
@Override
public CompressedInputStream read(int x, int z) throws IOException {
return item(x, z).read();
public @Nullable CompressedInputStream read(int x, int z) throws IOException {
return cell(x, z).read();
}
@Override
public void delete(int x, int z) throws IOException {
item(x, z).delete();
cell(x, z).delete();
}
@Override
public boolean exists(int x, int z) throws IOException {
return item(x, z).exists();
return cell(x, z).exists();
}
@Override
public ItemStorage cell(int x, int z) {
return new FileItemStorage(getItemPath(x, z), compression, atomic);
}
@SuppressWarnings("resource")
@Override
public Stream<Cell> stream() throws IOException {
return storage.files(root)
if (!Files.exists(root)) return Stream.empty();
return Files.walk(root)
.filter(Files::isRegularFile)
.<Cell>map(itemPath -> {
Path path = itemPath;
if (!path.startsWith(root)) return null;
path = root.relativize(path);
String name = path.toString();
name = name.replace(root.getFileSystem().getSeparator(), "");
if (!name.endsWith(suffix)) return null;
name = name.substring(name.length() - suffix.length());
name = name.substring(0, name.length() - suffix.length());
name = name.replace(root.getFileSystem().getSeparator(), "");
Matcher matcher = ITEM_PATH_PATTERN.matcher(name);
if (!matcher.matches()) return null;
int x = Integer.parseInt(matcher.group(1));
int z = Integer.parseInt(matcher.group(2));
return new PathCell(x, z, itemPath);
return new PathCell(x, z, itemPath, compression, atomic);
})
.filter(Objects::nonNull);
}
@Override
public boolean isClosed() {
return storage.isClosed();
return false;
}
public SingleItemStorage item(int x, int z) {
return storage.file(root.resolve(getGridPath(x, z)), compression);
}
public Path getGridPath(int x, int z) {
public Path getItemPath(int x, int z) {
StringBuilder sb = new StringBuilder()
.append('x')
.append(x)
@ -111,59 +117,34 @@ public Path getGridPath(int x, int z) {
LinkedList<String> folders = new LinkedList<>();
StringBuilder folder = new StringBuilder();
sb.chars().forEach(i -> {
char c = (char) i;
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
folder.append(c);
if (c >= '0' && c <= '9') {
folders.add(folder.toString());
folder.delete(0, folder.length());
}
});
}
String fileName = folders.removeLast();
folders.add(fileName + suffix);
return Path.of(folders.removeFirst(), folders.toArray(String[]::new));
Path gridPath = root;
for (String part : folders)
gridPath = gridPath.resolve(part);
return gridPath;
}
@RequiredArgsConstructor
private class PathCell implements Cell {
private static class PathCell extends FileItemStorage implements Cell {
@Getter
private final int x, z;
private final Path path;
private SingleItemStorage storage;
@Override
public OutputStream write() throws IOException {
return storage().write();
}
@Override
public CompressedInputStream read() throws IOException {
return storage().read();
}
@Override
public void delete() throws IOException {
storage().delete();
}
@Override
public boolean exists() throws IOException {
return storage().exists();
}
@Override
public boolean isClosed() {
return PathBasedGridStorage.this.isClosed();
}
private SingleItemStorage storage() {
if (storage == null)
storage = PathBasedGridStorage.this.storage.file(path, compression);
return storage;
public PathCell(int x, int z, Path itemPath, Compression compression, boolean atomic) {
super(itemPath, compression, atomic);
this.x = x;
this.z = z;
}
}

View File

@ -0,0 +1,61 @@
package de.bluecolored.bluemap.core.storage.file;
import de.bluecolored.bluemap.core.storage.ItemStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.util.FileHelper;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@RequiredArgsConstructor
public class FileItemStorage implements ItemStorage {
private final Path file;
private final Compression compression;
private final boolean atomic;
@Override
public OutputStream write() throws IOException {
if (atomic)
return compression.compress(FileHelper.createFilepartOutputStream(file));
Path folder = file.toAbsolutePath().normalize().getParent();
FileHelper.createDirectories(folder);
return compression.compress(Files.newOutputStream(file,
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE));
}
@Override
public @Nullable CompressedInputStream read() throws IOException {
if (!Files.exists(file)) return null;
try {
return new CompressedInputStream(Files.newInputStream(file), compression);
} catch (FileNotFoundException | NoSuchFileException ex) {
return null;
}
}
@Override
public void delete() throws IOException {
if (Files.exists(file)) Files.delete(file);
}
@Override
public boolean exists() {
return Files.exists(file);
}
@Override
public boolean isClosed() {
return false;
}
}

View File

@ -24,49 +24,126 @@
*/
package de.bluecolored.bluemap.core.storage.file;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.ItemStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import de.bluecolored.bluemap.core.util.DeletingPathVisitor;
import de.bluecolored.bluemap.core.util.FileHelper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.function.DoublePredicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Getter
public class FileMapStorage extends PathBasedMapStorage {
public class FileMapStorage implements MapStorage {
private static final String TILES_PATH = "tiles";
private static final String RENDER_STATE_PATH = "rstate";
private static final String LIVE_PATH = "live";
private final Path root;
private final Compression compression;
private final boolean atomic;
public FileMapStorage(Path root, Compression compression) {
super(
compression,
".prbm",
".png"
);
private final GridStorage hiresGridStorage;
private final LoadingCache<Integer, GridStorage> lowresGridStorages;
private final GridStorage tileStateStorage;
private final GridStorage chunkStateStorage;
public FileMapStorage(Path root, Compression compression, boolean atomic) {
this.root = root;
this.compression = compression;
this.atomic = atomic;
this.hiresGridStorage = new FileGridStorage(
root.resolve(TILES_PATH).resolve("0"),
".prbm" + compression.getFileSuffix(),
compression,
atomic
);
this.lowresGridStorages = Caffeine.newBuilder().build(lod -> new FileGridStorage(
root.resolve(TILES_PATH).resolve(String.valueOf(lod)),
".png",
Compression.NONE,
atomic
));
this.tileStateStorage = new FileGridStorage(
root.resolve(RENDER_STATE_PATH),
".tiles.dat",
Compression.GZIP,
atomic
);
this.chunkStateStorage = new FileGridStorage(
root.resolve(RENDER_STATE_PATH).resolve(""),
".chunks.dat",
Compression.GZIP,
atomic
);
}
@Override
public SingleItemStorage file(Path file, Compression compression) {
return new FileItemStorage(root.resolve(file), compression);
public GridStorage hiresTiles() {
return hiresGridStorage;
}
@Override
@SuppressWarnings("resource")
public Stream<Path> files(Path path) throws IOException {
return Files.walk(root.resolve(path))
.filter(Files::isRegularFile);
public GridStorage lowresTiles(int lod) {
return lowresGridStorages.get(lod);
}
@Override
public GridStorage tileState() {
return tileStateStorage;
}
@Override
public GridStorage chunkState() {
return chunkStateStorage;
}
public Path getAssetPath(String name) {
String[] parts = MapStorage.escapeAssetName(name)
.split("/");
Path assetPath = root.resolve("assets");
for (String part : parts)
assetPath = assetPath.resolve(part);
return assetPath;
}
@Override
public ItemStorage asset(String name) {
return new FileItemStorage(getAssetPath(name), Compression.NONE, atomic);
}
@Override
public ItemStorage settings() {
return new FileItemStorage(root.resolve("settings.json"), Compression.NONE, atomic);
}
@Override
public ItemStorage textures() {
return new FileItemStorage(root.resolve("textures.json" + compression.getFileSuffix()), compression, atomic);
}
@Override
public ItemStorage markers() {
return new FileItemStorage(root.resolve(LIVE_PATH).resolve("markers.json"), Compression.NONE, atomic);
}
@Override
public ItemStorage players() {
return new FileItemStorage(root.resolve(LIVE_PATH).resolve("players.json"), Compression.NONE, atomic);
}
@Override
@ -107,42 +184,4 @@ public boolean isClosed() {
return false;
}
@RequiredArgsConstructor
private static class FileItemStorage implements SingleItemStorage {
private final Path file;
private final Compression compression;
@Override
public OutputStream write() throws IOException {
return compression.compress(FileHelper.createFilepartOutputStream(file));
}
@Override
public CompressedInputStream read() throws IOException {
if (!Files.exists(file)) return null;
try {
return new CompressedInputStream(Files.newInputStream(file), compression);
} catch (FileNotFoundException | NoSuchFileException ex) {
return null;
}
}
@Override
public void delete() throws IOException {
Files.delete(file);
}
@Override
public boolean exists() {
return Files.exists(file);
}
@Override
public boolean isClosed() {
return false;
}
}
}

View File

@ -39,11 +39,11 @@ public class FileStorage implements Storage {
private final Path root;
private final LoadingCache<String, FileMapStorage> mapStorages;
public FileStorage(Path root, Compression compression) {
public FileStorage(Path root, Compression compression, boolean atomic) {
this.root = root;
mapStorages = Caffeine.newBuilder()
.build(id -> new FileMapStorage(root.resolve(id), compression));
.build(id -> new FileMapStorage(root.resolve(id), compression, atomic));
}
@Override

View File

@ -1,126 +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.storage.file;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Path;
import java.util.stream.Stream;
public abstract class PathBasedMapStorage implements MapStorage {
public static final Path SETTINGS_PATH = Path.of("settings.json");
public static final Path TEXTURES_PATH = Path.of("textures.json");
public static final Path RENDER_STATE_PATH = Path.of(".rstate");
public static final Path MARKERS_PATH = Path.of("live", "markers.json");
public static final Path PLAYERS_PATH = Path.of("live", "players.json");
private final GridStorage hiresGridStorage;
private final LoadingCache<Integer, GridStorage> lowresGridStorages;
public PathBasedMapStorage(Compression compression, String hiresSuffix, String lowresSuffix) {
this.hiresGridStorage = new PathBasedGridStorage(
this,
Path.of("tiles", "0"),
hiresSuffix + compression.getFileSuffix(),
compression
);
this.lowresGridStorages = Caffeine.newBuilder().build(lod -> new PathBasedGridStorage(
this,
Path.of("tiles", String.valueOf(lod)),
lowresSuffix,
Compression.NONE
));
}
@Override
public GridStorage hiresTiles() {
return hiresGridStorage;
}
@Override
public GridStorage lowresTiles(int lod) {
return lowresGridStorages.get(lod);
}
public Path getAssetPath(String name) {
String[] parts = MapStorage.escapeAssetName(name)
.split("/");
return Path.of("assets", parts);
}
@Override
public SingleItemStorage asset(String name) {
return file(getAssetPath(name), Compression.NONE);
}
@Override
public SingleItemStorage renderState() {
return file(RENDER_STATE_PATH, Compression.NONE);
}
@Override
public SingleItemStorage settings() {
return file(SETTINGS_PATH, Compression.NONE);
}
@Override
public SingleItemStorage textures() {
return file(TEXTURES_PATH, Compression.NONE);
}
@Override
public SingleItemStorage markers() {
return file(MARKERS_PATH, Compression.NONE);
}
@Override
public SingleItemStorage players() {
return file(PLAYERS_PATH, Compression.NONE);
}
/**
* Returns a {@link SingleItemStorage} for a file with the given path and compression.
* The file does not have to actually exist.
*/
public abstract SingleItemStorage file(Path file, Compression compression);
/**
* Returns a stream with all file-paths of existing files at or below the given path.
* (Including files in potential sub-folders)<br>
* Basically, this method should mimic the functionality of
* {@link java.nio.file.Files#walk(Path, FileVisitOption...)}
*/
public abstract Stream<Path> files(Path path) throws IOException;
}

View File

@ -78,6 +78,7 @@ public <R> R run(ConnectionFunction<R> action) throws IOException {
SQLException sqlException = null;
try {
// try the action 2 times if a "recoverable" exception is thrown
for (int i = 0; i < 2; i++) {
try (Connection connection = dataSource.getConnection()) {
try {

View File

@ -24,10 +24,12 @@
*/
package de.bluecolored.bluemap.core.storage.sql;
import de.bluecolored.bluemap.core.storage.ItemStorage;
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.stream.OnCloseOutputStream;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
@ -37,36 +39,41 @@
import java.util.stream.StreamSupport;
@RequiredArgsConstructor
public class SQLTileStorage implements GridStorage {
public class SQLGridStorage implements GridStorage {
private final CommandSet sql;
private final String mapId;
private final int lod;
private final String map;
private final Key storage;
private final Compression compression;
@Override
public OutputStream write(int x, int z) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
return new OnCloseOutputStream(compression.compress(bytes),
() -> sql.writeMapTile(mapId, lod, x, z, compression, bytes.toByteArray())
() -> sql.writeGridItem(map, storage, x, z, compression, bytes.toByteArray())
);
}
@Override
public @Nullable CompressedInputStream read(int x, int z) throws IOException {
byte[] data = sql.readMapTile(mapId, lod, x, z, compression);
byte[] data = sql.readGridItem(map, storage, x, z, compression);
if (data == null) return null;
return new CompressedInputStream(new ByteArrayInputStream(data), compression);
}
@Override
public void delete(int x, int z) throws IOException {
sql.deleteMapTile(mapId, lod, x, z, compression);
sql.deleteGridItem(map, storage, x, z);
}
@Override
public boolean exists(int x, int z) throws IOException {
return sql.hasMapTile(mapId, lod, x, z, compression);
return sql.hasGridItem(map, storage, x, z, compression);
}
@Override
public ItemStorage cell(int x, int z) {
return new GridStorageCell(this, x, z);
}
@Override
@ -74,7 +81,7 @@ public Stream<Cell> stream() throws IOException {
return StreamSupport.stream(
new PageSpliterator<>(page -> {
try {
return sql.listMapTiles(mapId, lod, compression, page * 1000, 1000);
return sql.listGridItems(map, storage, compression, page * 1000, 1000);
} catch (IOException ex) { throw new RuntimeException(ex); }
}),
false

View File

@ -26,8 +26,9 @@
import de.bluecolored.bluemap.core.storage.compression.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import de.bluecolored.bluemap.core.storage.ItemStorage;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.stream.OnCloseOutputStream;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
@ -35,36 +36,36 @@
import java.io.*;
@RequiredArgsConstructor
public class SQLMetaItemStorage implements SingleItemStorage {
public class SQLItemStorage implements ItemStorage {
private final CommandSet sql;
private final String mapId;
private final String itemName;
private final String map;
private final Key storage;
private final Compression compression;
@Override
public OutputStream write() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
return new OnCloseOutputStream(compression.compress(bytes),
() -> sql.writeMapMeta(mapId, itemName, bytes.toByteArray())
() -> sql.writeItem(map, storage, compression, bytes.toByteArray())
);
}
@Override
public @Nullable CompressedInputStream read() throws IOException {
byte[] data = sql.readMapMeta(mapId, itemName);
byte[] data = sql.readItem(map, storage, compression);
if (data == null) return null;
return new CompressedInputStream(new ByteArrayInputStream(data), compression);
}
@Override
public void delete() throws IOException {
sql.deleteMapMeta(mapId, itemName);
sql.deleteItem(map, storage);
}
@Override
public boolean exists() throws IOException {
return sql.hasMapMeta(mapId, itemName);
return sql.hasItem(map, storage, compression);
}
@Override

View File

@ -24,116 +24,53 @@
*/
package de.bluecolored.bluemap.core.storage.sql;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.GridStorage;
import de.bluecolored.bluemap.core.storage.MapStorage;
import de.bluecolored.bluemap.core.storage.SingleItemStorage;
import de.bluecolored.bluemap.core.storage.ItemStorage;
import de.bluecolored.bluemap.core.storage.KeyedMapStorage;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.commandset.CommandSet;
import de.bluecolored.bluemap.core.util.Key;
import java.io.IOException;
import java.util.function.DoublePredicate;
public class SQLMapStorage implements MapStorage {
public static final String SETTINGS_META_NAME = "settings.json";
public static final String TEXTURES_META_NAME = "textures.json";
public static final String RENDER_STATE_META_NAME = ".rstate";
public static final String MARKERS_META_NAME = "live/markers.json";
public static final String PLAYERS_META_NAME = "live/players.json";
public class SQLMapStorage extends KeyedMapStorage {
private final String mapId;
private final CommandSet sql;
private final SQLTileStorage hiresTileStorage;
private final LoadingCache<Integer, GridStorage> lowresGridStorages;
private final SingleItemStorage renderStateStorage;
private final SingleItemStorage settingsStorage;
private final SingleItemStorage texturesStorage;
private final SingleItemStorage markersStorage;
private final SingleItemStorage playersStorage;
private final Cache<Key, ItemStorage> itemStorages = Caffeine.newBuilder().build();
private final Cache<Key, GridStorage> gridStorages = Caffeine.newBuilder().build();
public SQLMapStorage(String mapId, CommandSet sql, Compression compression) {
super(compression);
this.mapId = mapId;
this.sql = sql;
this.hiresTileStorage = new SQLTileStorage(
sql,
mapId,
0,
compression
);
this.lowresGridStorages = Caffeine.newBuilder().build(lod -> new SQLTileStorage(
sql,
mapId,
lod,
Compression.NONE
));
renderStateStorage = meta(RENDER_STATE_META_NAME, Compression.NONE);
settingsStorage = meta(SETTINGS_META_NAME, Compression.NONE);
texturesStorage = meta(TEXTURES_META_NAME, Compression.NONE);
markersStorage = meta(MARKERS_META_NAME, Compression.NONE);
playersStorage = meta(PLAYERS_META_NAME, Compression.NONE);
}
@Override
public GridStorage hiresTiles() {
return hiresTileStorage;
public ItemStorage item(Key key, Compression compression) {
return itemStorages.get(key, k -> new SQLItemStorage(sql, mapId, key, compression));
}
@Override
public GridStorage lowresTiles(int lod) {
return lowresGridStorages.get(lod);
}
public String getAssetMetaName(String assetName) {
return "assets/" + MapStorage.escapeAssetName(assetName);
}
@Override
public SingleItemStorage asset(String name) {
return meta(getAssetMetaName(name), Compression.NONE);
}
@Override
public SingleItemStorage renderState() {
return renderStateStorage;
}
@Override
public SingleItemStorage settings() {
return settingsStorage;
}
@Override
public SingleItemStorage textures() {
return texturesStorage;
}
@Override
public SingleItemStorage markers() {
return markersStorage;
}
@Override
public SingleItemStorage players() {
return playersStorage;
public GridStorage grid(Key key, Compression compression) {
return gridStorages.get(key, k -> new SQLGridStorage(sql, mapId, key, compression));
}
@Override
public void delete(DoublePredicate onProgress) throws IOException {
// delete tiles in 1000er steps to track progress
int tileCount = sql.countAllMapTiles(mapId);
int tileCount = sql.countMapGridsItems(mapId);
if (tileCount > 0) {
int totalDeleted = 0;
int deleted = 0;
do {
deleted = sql.purgeMapTiles(mapId, 1000);
deleted = sql.purgeMapGrids(mapId, 1000);
totalDeleted += deleted;
if (!onProgress.test((double) totalDeleted / tileCount))
@ -152,10 +89,6 @@ public boolean exists() throws IOException {
return sql.hasMap(mapId);
}
private SingleItemStorage meta(String name, Compression compression) {
return new SQLMetaItemStorage(sql, mapId, name, compression);
}
@Override
public boolean isClosed() {
return sql.isClosed();

View File

@ -28,6 +28,7 @@
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.storage.sql.Database;
import de.bluecolored.bluemap.core.util.Key;
import lombok.RequiredArgsConstructor;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Nullable;
@ -41,12 +42,16 @@
@RequiredArgsConstructor
public abstract class AbstractCommandSet implements CommandSet {
private final Database db;
protected final Database db;
final LoadingCache<String, Integer> mapKeys = Caffeine.newBuilder()
protected final LoadingCache<String, Integer> mapKeys = Caffeine.newBuilder()
.build(this::findOrCreateMapKey);
final LoadingCache<Compression, Integer> compressionKeys = Caffeine.newBuilder()
protected final LoadingCache<Compression, Integer> compressionKeys = Caffeine.newBuilder()
.build(this::findOrCreateCompressionKey);
protected final LoadingCache<Key, Integer> itemStorageKeys = Caffeine.newBuilder()
.build(this::findOrCreateItemStorageKey);
protected final LoadingCache<Key, Integer> gridStorageKeys = Caffeine.newBuilder()
.build(this::findOrCreateGridStorageKey);
@Language("sql")
public abstract String createMapTableStatement();
@ -55,53 +60,55 @@ public abstract class AbstractCommandSet implements CommandSet {
public abstract String createCompressionTableStatement();
@Language("sql")
public abstract String createMapMetaTableStatement();
public abstract String createItemStorageTableStatement();
@Language("sql")
public abstract String createMapTileTableStatement();
public abstract String createItemStorageDataTableStatement();
@Language("sql")
public abstract String fixLegacyCompressionIdsStatement();
public abstract String createGridStorageTableStatement();
@Language("sql")
public abstract String createGridStorageDataTableStatement();
public void initializeTables() throws IOException {
db.run(connection -> {
executeUpdate(connection, createMapTableStatement());
executeUpdate(connection, createCompressionTableStatement());
executeUpdate(connection, createMapMetaTableStatement());
executeUpdate(connection, createMapTileTableStatement());
executeUpdate(connection, createItemStorageTableStatement());
executeUpdate(connection, createItemStorageDataTableStatement());
executeUpdate(connection, createGridStorageTableStatement());
executeUpdate(connection, createGridStorageDataTableStatement());
});
db.run(connection -> executeUpdate(connection, fixLegacyCompressionIdsStatement()));
}
@Language("sql")
public abstract String writeMapTileStatement();
public abstract String itemStorageWriteStatement();
@Override
public int writeMapTile(
String mapId, int lod, int x, int z, Compression compression,
byte[] bytes
) throws IOException {
public void writeItem(String mapId, Key key, Compression compression, byte[] bytes) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = itemStorageKey(key);
int compressionKey = compressionKey(compression);
db.run(connection -> executeUpdate(connection,
itemStorageWriteStatement(),
mapKey, storageKey, compressionKey,
bytes
));
}
@Language("sql")
public abstract String itemStorageReadStatement();
@Override
public byte @Nullable [] readItem(String mapId, Key key, Compression compression) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = itemStorageKey(key);
int compressionKey = compressionKey(compression);
return db.run(connection -> {
return executeUpdate(connection,
writeMapTileStatement(),
mapKey, lod, x, z, compressionKey,
bytes
);
});
}
@Language("sql")
public abstract String readMapTileStatement();
@Override
public byte @Nullable [] readMapTile(String mapId, int lod, int x, int z, Compression compression) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
readMapTileStatement(),
mapId, lod, x, z, compression.getKey().getFormatted()
itemStorageReadStatement(),
mapKey, storageKey, compressionKey
);
if (!result.next()) return null;
return result.getBytes(1);
@ -109,29 +116,30 @@ public int writeMapTile(
}
@Language("sql")
public abstract String deleteMapTileStatement();
public abstract String itemStorageDeleteStatement();
@Override
public int deleteMapTile(String mapId, int lod, int x, int z, Compression compression) throws IOException {
public void deleteItem(String mapId, Key key) throws IOException {
int mapKey = mapKey(mapId);
int compressionKey = compressionKey(compression);
return db.run(connection -> {
return executeUpdate(connection,
deleteMapTileStatement(),
mapKey, lod, x, z, compressionKey
);
});
int storageKey = itemStorageKey(key);
db.run(connection -> executeUpdate(connection,
itemStorageDeleteStatement(),
mapKey, storageKey
));
}
@Language("sql")
public abstract String hasMapTileStatement();
public abstract String itemStorageHasStatement();
@Override
public boolean hasMapTile(String mapId, int lod, int x, int z, Compression compression) throws IOException {
public boolean hasItem(String mapId, Key key, Compression compression) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = itemStorageKey(key);
int compressionKey = compressionKey(compression);
return db.run(connection -> {
ResultSet result = executeQuery(connection,
hasMapTileStatement(),
mapId, lod, x, z, compression.getKey().getFormatted()
itemStorageHasStatement(),
mapKey, storageKey, compressionKey
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getBoolean(1);
@ -139,43 +147,93 @@ public boolean hasMapTile(String mapId, int lod, int x, int z, Compression compr
}
@Language("sql")
public abstract String countAllMapTilesStatement();
public abstract String gridStorageWriteStatement();
@Override
public int countAllMapTiles(String mapId) throws IOException {
public void writeGridItem(
String mapId, Key key, int x, int z, Compression compression,
byte[] bytes
) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = gridStorageKey(key);
int compressionKey = compressionKey(compression);
db.run(connection -> executeUpdate(connection,
gridStorageWriteStatement(),
mapKey, storageKey, x, z, compressionKey,
bytes
));
}
@Language("sql")
public abstract String gridStorageReadStatement();
@Override
public byte @Nullable [] readGridItem(
String mapId, Key key, int x, int z, Compression compression
) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = gridStorageKey(key);
int compressionKey = compressionKey(compression);
return db.run(connection -> {
ResultSet result = executeQuery(connection,
countAllMapTilesStatement(),
mapId
gridStorageReadStatement(),
mapKey, storageKey, x, z, compressionKey
);
if (!result.next()) return null;
return result.getBytes(1);
});
}
@Language("sql")
public abstract String gridStorageDeleteStatement();
@Override
public void deleteGridItem(
String mapId, Key key, int x, int z
) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = gridStorageKey(key);
db.run(connection -> executeUpdate(connection,
gridStorageDeleteStatement(),
mapKey, storageKey, x, z
));
}
@Language("sql")
public abstract String gridStorageHasStatement();
@Override
public boolean hasGridItem(
String mapId, Key key, int x, int z, Compression compression
) throws IOException {
int mapKey = mapKey(mapId);
int storageKey = gridStorageKey(key);
int compressionKey = compressionKey(compression);
return db.run(connection -> {
ResultSet result = executeQuery(connection,
gridStorageHasStatement(),
mapKey, storageKey, x, z, compressionKey
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getInt(1);
return result.getBoolean(1);
});
}
@Language("sql")
public abstract String purgeMapTilesStatement();
public abstract String gridStorageListStatement();
@Override
public int purgeMapTiles(String mapId, int limit) throws IOException {
public TilePosition[] listGridItems(
String mapId, Key key, Compression compression,
int start, int count
) throws IOException {
int mapKey = mapKey(mapId);
return db.run(connection -> {
return executeUpdate(connection,
purgeMapTilesStatement(),
mapKey, limit
);
});
}
@Language("sql")
public abstract String listMapTilesStatement();
@Override
public TilePosition[] listMapTiles(String mapId, int lod, Compression compression, int start, int count) throws IOException {
int storageKey = gridStorageKey(key);
int compressionKey = compressionKey(compression);
return db.run(connection -> {
ResultSet result = executeQuery(connection,
listMapTilesStatement(),
mapId, lod, compression.getKey().getFormatted(),
gridStorageListStatement(),
mapKey, storageKey, compressionKey,
count, start
);
@ -199,96 +257,46 @@ public TilePosition[] listMapTiles(String mapId, int lod, Compression compressio
}
@Language("sql")
public abstract String writeMapMetaStatement();
public abstract String gridStorageCountMapItemsStatement();
@Override
public int writeMapMeta(String mapId, String itemName, byte[] bytes) throws IOException {
public int countMapGridsItems(String mapId) throws IOException {
int mapKey = mapKey(mapId);
return db.run(connection -> {
return executeUpdate(connection,
writeMapMetaStatement(),
mapKey, itemName,
bytes
);
});
}
@Language("sql")
public abstract String readMapMetaStatement();
@Override
public byte @Nullable [] readMapMeta(String mapId, String itemName) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
readMapMetaStatement(),
mapId, itemName
);
if (!result.next()) return null;
return result.getBytes(1);
});
}
@Language("sql")
public abstract String deleteMapMetaStatement();
@Override
public int deleteMapMeta(String mapId, String itemName) throws IOException {
int mapKey = mapKey(mapId);
return db.run(connection -> {
return executeUpdate(connection,
deleteMapMetaStatement(),
mapKey, itemName
);
});
}
@Language("sql")
public abstract String hasMapMetaStatement();
@Override
public boolean hasMapMeta(String mapId, String itemName) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
hasMapMetaStatement(),
mapId, itemName
gridStorageCountMapItemsStatement(),
mapKey
);
if (!result.next()) throw new IllegalStateException("Counting query returned empty result!");
return result.getBoolean(1);
return result.getInt(1);
});
}
@Language("sql")
public abstract String purgeMapTileTableStatement();
public abstract String gridStoragePurgeMapStatement();
@Override
public int purgeMapGrids(String mapId, int limit) throws IOException {
int mapKey = mapKey(mapId);
return db.run(connection -> {
return executeUpdate(connection,
gridStoragePurgeMapStatement(),
mapKey, limit
);
});
}
@Language("sql")
public abstract String purgeMapMetaTableStatement();
@Language("sql")
public abstract String deleteMapStatement();
public abstract String purgeMapStatement();
@Override
public void purgeMap(String mapId) throws IOException {
synchronized (mapKeys) {
int mapKey = mapKey(mapId);
db.run(connection -> {
executeUpdate(connection,
purgeMapTileTableStatement(),
db.run(connection -> executeUpdate(connection,
purgeMapStatement(),
mapKey
);
executeUpdate(connection,
purgeMapMetaTableStatement(),
mapKey
);
executeUpdate(connection,
deleteMapStatement(),
mapKey
);
});
));
mapKeys.invalidate(mapId);
}
}
@ -398,6 +406,78 @@ public int findOrCreateCompressionKey(Compression compression) throws IOExceptio
});
}
@Language("sql")
public abstract String findItemStorageKeyStatement();
@Language("sql")
public abstract String createItemStorageKeyStatement();
public int itemStorageKey(Key key) {
synchronized (itemStorageKeys) {
//noinspection DataFlowIssue
return itemStorageKeys.get(key);
}
}
public int findOrCreateItemStorageKey(Key key) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
findItemStorageKeyStatement(),
key.getFormatted()
);
if (result.next())
return result.getInt(1);
PreparedStatement statement = connection.prepareStatement(
createItemStorageKeyStatement(),
Statement.RETURN_GENERATED_KEYS
);
statement.setString(1, key.getFormatted());
statement.executeUpdate();
ResultSet keys = statement.getGeneratedKeys();
if (!keys.next()) throw new IllegalStateException("No generated key returned!");
return keys.getInt(1);
});
}
@Language("sql")
public abstract String findGridStorageKeyStatement();
@Language("sql")
public abstract String createGridStorageKeyStatement();
public int gridStorageKey(Key key) {
synchronized (gridStorageKeys) {
//noinspection DataFlowIssue
return gridStorageKeys.get(key);
}
}
public int findOrCreateGridStorageKey(Key key) throws IOException {
return db.run(connection -> {
ResultSet result = executeQuery(connection,
findGridStorageKeyStatement(),
key.getFormatted()
);
if (result.next())
return result.getInt(1);
PreparedStatement statement = connection.prepareStatement(
createGridStorageKeyStatement(),
Statement.RETURN_GENERATED_KEYS
);
statement.setString(1, key.getFormatted());
statement.executeUpdate();
ResultSet keys = statement.getGeneratedKeys();
if (!keys.next()) throw new IllegalStateException("No generated key returned!");
return keys.getInt(1);
});
}
@Override
public boolean isClosed() {
return db.isClosed();

View File

@ -25,6 +25,7 @@
package de.bluecolored.bluemap.core.storage.sql.commandset;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.util.Key;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
@ -34,39 +35,39 @@ public interface CommandSet extends Closeable {
void initializeTables() throws IOException;
int writeMapTile(
String mapId, int lod, int x, int z, Compression compression,
void writeItem(String mapId, Key key, Compression compression, byte[] bytes) throws IOException;
byte @Nullable [] readItem(String mapId, Key key, Compression compression) throws IOException;
void deleteItem(String mapId, Key key) throws IOException;
boolean hasItem(String mapId, Key key, Compression compression) throws IOException;
void writeGridItem(
String mapId, Key key, int x, int z, Compression compression,
byte[] bytes
) throws IOException;
byte @Nullable [] readMapTile(
String mapId, int lod, int x, int z, Compression compression
byte @Nullable [] readGridItem(
String mapId, Key key, int x, int z, Compression compression
) throws IOException;
int deleteMapTile(
String mapId, int lod, int x, int z, Compression compression
void deleteGridItem(
String mapId, Key key, int x, int z
) throws IOException;
boolean hasMapTile(
String mapId, int lod, int x, int z, Compression compression
boolean hasGridItem(
String mapId, Key key, int x, int z, Compression compression
) throws IOException;
TilePosition[] listMapTiles(
String mapId, int lod, Compression compression,
TilePosition[] listGridItems(
String mapId, Key key, Compression compression,
int start, int count
) throws IOException;
int countAllMapTiles(String mapId) throws IOException;
int countMapGridsItems(String mapId) throws IOException;
int purgeMapTiles(String mapId, int limit) throws IOException;
int writeMapMeta(String mapId, String itemName, byte[] bytes) throws IOException;
byte @Nullable [] readMapMeta(String mapId, String itemName) throws IOException;
int deleteMapMeta(String mapId, String itemName) throws IOException;
boolean hasMapMeta(String mapId, String itemName) throws IOException;
int purgeMapGrids(String mapId, int limit) throws IOException;
void purgeMap(String mapId) throws IOException;

View File

@ -50,53 +50,51 @@ PRIMARY KEY (`id`),
@Language("mysql")
public String createCompressionTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile_compression` (
CREATE TABLE IF NOT EXISTS `bluemap_compression` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
`compression` VARCHAR(190) NOT NULL,
`key` VARCHAR(190) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `compression` (`compression`)
UNIQUE INDEX `key` (`key`)
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String createMapMetaTableStatement() {
public String createItemStorageTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_meta` (
`map` SMALLINT UNSIGNED NOT NULL,
`key` varchar(190) NOT NULL,
`value` LONGBLOB NOT NULL,
PRIMARY KEY (`map`, `key`),
CONSTRAINT `fk_bluemap_map_meta_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
CREATE TABLE IF NOT EXISTS `bluemap_item_storage` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`key` VARCHAR(190) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `key` (`key`)
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String createMapTileTableStatement() {
public String createItemStorageDataTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile` (
CREATE TABLE IF NOT EXISTS `bluemap_item_storage_data` (
`map` SMALLINT UNSIGNED NOT NULL,
`lod` SMALLINT UNSIGNED NOT NULL,
`x` INT NOT NULL,
`z` INT NOT NULL,
`storage` INT UNSIGNED NOT NULL,
`compression` SMALLINT UNSIGNED NOT NULL,
`data` LONGBLOB NOT NULL,
PRIMARY KEY (`map`, `lod`, `x`, `z`),
CONSTRAINT `fk_bluemap_map_tile_map`
PRIMARY KEY (`map`, `storage`),
CONSTRAINT `fk_bluemap_item_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_map_tile_compression`
CONSTRAINT `fk_bluemap_item`
FOREIGN KEY (`storage`)
REFERENCES `bluemap_item_storage` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_item_compression`
FOREIGN KEY (`compression`)
REFERENCES `bluemap_map_tile_compression` (`id`)
REFERENCES `bluemap_compression` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) COLLATE 'utf8mb4_bin'
@ -105,50 +103,112 @@ FOREIGN KEY (`compression`)
@Override
@Language("mysql")
public String fixLegacyCompressionIdsStatement() {
public String createGridStorageTableStatement() {
return """
UPDATE IGNORE `bluemap_map_tile_compression`
SET `compression` = CONCAT('bluemap:', `compression`)
WHERE NOT `compression` LIKE '%:%'
CREATE TABLE IF NOT EXISTS `bluemap_grid_storage` (
`id` SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
`key` VARCHAR(190) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `key` (`key`)
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String writeMapTileStatement() {
public String createGridStorageDataTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_grid_storage_data` (
`map` SMALLINT UNSIGNED NOT NULL,
`storage` SMALLINT UNSIGNED NOT NULL,
`x` INT NOT NULL,
`z` INT NOT NULL,
`compression` SMALLINT UNSIGNED NOT NULL,
`data` LONGBLOB NOT NULL,
PRIMARY KEY (`map`, `storage`, `x`, `z`),
CONSTRAINT `fk_bluemap_grid_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_grid`
FOREIGN KEY (`storage`)
REFERENCES `bluemap_grid_storage` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_grid_compression`
FOREIGN KEY (`compression`)
REFERENCES `bluemap_compression` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) COLLATE 'utf8mb4_bin'
""";
}
@Override
@Language("mysql")
public String itemStorageWriteStatement() {
return """
REPLACE
INTO `bluemap_map_tile` (`map`, `lod`, `x`, `z`, `compression`, `data`)
INTO `bluemap_item_storage_data` (`map`, `storage`, `compression`, `data`)
VALUES (?, ?, ?, ?)
""";
}
@Override
@Language("mysql")
public String itemStorageReadStatement() {
return """
SELECT `data`
FROM `bluemap_item_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `compression` = ?
""";
}
@Override
@Language("mysql")
public String itemStorageDeleteStatement() {
return """
DELETE
FROM `bluemap_item_storage_data`
WHERE `map` = ?
AND `storage` = ?
""";
}
@Override
@Language("mysql")
public String itemStorageHasStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_item_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `compression` = ?
""";
}
@Override
@Language("mysql")
public String gridStorageWriteStatement() {
return """
REPLACE
INTO `bluemap_grid_storage_data` (`map`, `storage`, `x`, `z`, `compression`, `data`)
VALUES (?, ?, ?, ?, ?, ?)
""";
}
@Override
@Language("mysql")
public String readMapTileStatement() {
public String gridStorageReadStatement() {
return """
SELECT t.`data`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("mysql")
public String deleteMapTileStatement() {
return """
DELETE
FROM `bluemap_map_tile`
SELECT `data`
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `lod` = ?
AND `storage` = ?
AND `x` = ?
AND `z` = ?
AND `compression` = ?
@ -157,40 +217,60 @@ public String deleteMapTileStatement() {
@Override
@Language("mysql")
public String hasMapTileStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("mysql")
public String countAllMapTilesStatement() {
return """
SELECT COUNT(*)
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
""";
}
@Override
@Language("mysql")
public String purgeMapTilesStatement() {
public String gridStorageDeleteStatement() {
return """
DELETE
FROM `bluemap_map_tile`
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `x` = ?
AND `z` = ?
""";
}
@Override
@Language("mysql")
public String gridStorageHasStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `x` = ?
AND `z` = ?
AND `compression` = ?
""";
}
@Override
@Language("mysql")
public String gridStorageListStatement() {
return """
SELECT `x`, `z`
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `compression` = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("mysql")
public String gridStorageCountMapItemsStatement() {
return """
SELECT COUNT(*)
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
""";
}
@Override
@Language("mysql")
public String gridStoragePurgeMapStatement() {
return """
DELETE
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
LIMIT ?
""";
@ -198,95 +278,11 @@ public String purgeMapTilesStatement() {
@Override
@Language("mysql")
public String listMapTilesStatement() {
return """
SELECT t.`x`, t.`z`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND c.`compression` = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("mysql")
public String writeMapMetaStatement() {
return """
REPLACE
INTO `bluemap_map_meta` (`map`, `key`, `value`)
VALUES (?, ?, ?)
""";
}
@Override
@Language("mysql")
public String readMapMetaStatement() {
return """
SELECT t.`value`
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
""";
}
@Override
@Language("mysql")
public String deleteMapMetaStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` = ?
AND `key` = ?
""";
}
@Override
@Language("mysql")
public String hasMapMetaStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
""";
}
@Override
@Language("mysql")
public String purgeMapTileTableStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` = ?
""";
}
@Override
@Language("mysql")
public String purgeMapMetaTableStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` = ?
""";
}
@Override
@Language("mysql")
public String deleteMapStatement() {
public String purgeMapStatement() {
return """
DELETE
FROM `bluemap_map`
WHERE `map` = ?
WHERE `id` = ?
""";
}
@ -335,8 +331,8 @@ public String createMapKeyStatement() {
public String findCompressionKeyStatement() {
return """
SELECT `id`
FROM `bluemap_map_tile_compression`
WHERE `compression` = ?
FROM `bluemap_compression`
WHERE `key` = ?
""";
}
@ -345,7 +341,47 @@ public String findCompressionKeyStatement() {
public String createCompressionKeyStatement() {
return """
INSERT
INTO `bluemap_map_tile_compression` (`compression`)
INTO `bluemap_compression` (`key`)
VALUES (?)
""";
}
@Override
@Language("mysql")
public String findItemStorageKeyStatement() {
return """
SELECT `id`
FROM `bluemap_item_storage`
WHERE `key` = ?
""";
}
@Override
@Language("mysql")
public String createItemStorageKeyStatement() {
return """
INSERT
INTO `bluemap_item_storage` (`key`)
VALUES (?)
""";
}
@Override
@Language("mysql")
public String findGridStorageKeyStatement() {
return """
SELECT `id`
FROM `bluemap_grid_storage`
WHERE `key` = ?
""";
}
@Override
@Language("mysql")
public String createGridStorageKeyStatement() {
return """
INSERT
INTO `bluemap_grid_storage` (`key`)
VALUES (?)
""";
}

View File

@ -48,69 +48,91 @@ map_id VARCHAR(190) UNIQUE NOT NULL
@Language("postgresql")
public String createCompressionTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map_tile_compression (
CREATE TABLE IF NOT EXISTS bluemap_compression (
id SMALLSERIAL PRIMARY KEY,
compression VARCHAR(190) UNIQUE NOT NULL
key VARCHAR(190) UNIQUE NOT NULL
)
""";
}
@Override
@Language("postgresql")
public String createMapMetaTableStatement() {
public String createItemStorageTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map_meta (
map SMALLINT NOT NULL
REFERENCES bluemap_map(id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
key VARCHAR(190) NOT NULL,
value BYTEA NOT NULL,
PRIMARY KEY (map, key)
CREATE TABLE IF NOT EXISTS bluemap_item_storage (
id SERIAL PRIMARY KEY,
key VARCHAR(190) UNIQUE NOT NULL
)
""";
}
@Override
@Language("postgresql")
public String createMapTileTableStatement() {
public String createItemStorageDataTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_map_tile (
CREATE TABLE IF NOT EXISTS bluemap_item_storage_data (
map SMALLINT NOT NULL
REFERENCES bluemap_map (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
lod SMALLINT NOT NULL,
x INT NOT NULL,
z INT NOT NULL,
storage INT NOT NULL
REFERENCES bluemap_item_storage (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
compression SMALLINT NOT NULL
REFERENCES bluemap_map_tile_compression (id)
REFERENCES bluemap_compression (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
data BYTEA NOT NULL,
PRIMARY KEY (map, lod, x, z)
PRIMARY KEY (map, storage)
)
""";
}
@Override
@Language("postgresql")
public String fixLegacyCompressionIdsStatement() {
public String createGridStorageTableStatement() {
return """
UPDATE bluemap_map_tile_compression
SET compression = CONCAT('bluemap:', compression)
WHERE NOT compression LIKE '%:%'
CREATE TABLE IF NOT EXISTS bluemap_grid_storage (
id SMALLSERIAL PRIMARY KEY,
key VARCHAR(190) UNIQUE NOT NULL
)
""";
}
@Override
@Language("postgresql")
public String writeMapTileStatement() {
public String createGridStorageDataTableStatement() {
return """
CREATE TABLE IF NOT EXISTS bluemap_grid_storage_data (
map SMALLINT NOT NULL
REFERENCES bluemap_map (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
storage SMALLINT NOT NULL
REFERENCES bluemap_grid_storage (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
x INT NOT NULL,
z INT NOT NULL,
compression SMALLINT NOT NULL
REFERENCES bluemap_compression (id)
ON UPDATE RESTRICT
ON DELETE CASCADE,
data BYTEA NOT NULL,
PRIMARY KEY (map, storage, x, z)
)
""";
}
@Override
@Language("postgresql")
public String itemStorageWriteStatement() {
return """
INSERT
INTO bluemap_map_tile (map, lod, x, z, compression, data)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (map, lod, x, z)
INTO bluemap_item_storage_data (map, storage, compression, data)
VALUES (?, ?, ?, ?)
ON CONFLICT (map, storage)
DO UPDATE SET
compression = excluded.compression,
data = excluded.data
@ -119,30 +141,61 @@ ON CONFLICT (map, lod, x, z)
@Override
@Language("postgresql")
public String readMapTileStatement() {
public String itemStorageReadStatement() {
return """
SELECT t.data
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
WHERE m.map_id = ?
AND t.lod = ?
AND t.x = ?
AND t.z = ?
AND c.compression = ?
SELECT data
FROM bluemap_item_storage_data
WHERE map = ?
AND storage = ?
AND compression = ?
""";
}
@Override
@Language("postgresql")
public String deleteMapTileStatement() {
public String itemStorageDeleteStatement() {
return """
DELETE
FROM bluemap_map_tile
FROM bluemap_item_storage_data
WHERE map = ?
AND lod = ?
AND storage = ?
""";
}
@Override
@Language("postgresql")
public String itemStorageHasStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_item_storage_data
WHERE map = ?
AND storage = ?
AND compression = ?
""";
}
@Override
@Language("postgresql")
public String gridStorageWriteStatement() {
return """
INSERT
INTO bluemap_grid_storage_data (map, storage, x, z, compression, data)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (map, storage, x, z)
DO UPDATE SET
compression = excluded.compression,
data = excluded.data
""";
}
@Override
@Language("postgresql")
public String gridStorageReadStatement() {
return """
SELECT data
FROM bluemap_grid_storage_data
WHERE map = ?
AND storage = ?
AND x = ?
AND z = ?
AND compression = ?
@ -151,43 +204,63 @@ public String deleteMapTileStatement() {
@Override
@Language("postgresql")
public String hasMapTileStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
WHERE m.map_id = ?
AND t.lod = ?
AND t.x = ?
AND t.z = ?
AND c.compression = ?
""";
}
@Override
@Language("postgresql")
public String countAllMapTilesStatement() {
return """
SELECT COUNT(*)
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
""";
}
@Override
@Language("postgresql")
public String purgeMapTilesStatement() {
public String gridStorageDeleteStatement() {
return """
DELETE
FROM bluemap_map_tile
FROM bluemap_grid_storage_data
WHERE map = ?
AND storage = ?
AND x = ?
AND z = ?
""";
}
@Override
@Language("postgresql")
public String gridStorageHasStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_grid_storage_data
WHERE map = ?
AND storage = ?
AND x = ?
AND z = ?
AND compression = ?
""";
}
@Override
@Language("postgresql")
public String gridStorageListStatement() {
return """
SELECT x, z
FROM bluemap_grid_storage_data
WHERE map = ?
AND storage = ?
AND compression = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("postgresql")
public String gridStorageCountMapItemsStatement() {
return """
SELECT COUNT(*)
FROM bluemap_grid_storage_data
WHERE map = ?
""";
}
@Override
@Language("postgresql")
public String gridStoragePurgeMapStatement() {
return """
DELETE
FROM bluemap_grid_storage_data
WHERE CTID IN (
SELECT CTID
FROM bluemap_map_tile t
FROM bluemap_grid_storage_data t
WHERE t.map = ?
LIMIT ?
)
@ -196,98 +269,11 @@ WHERE CTID IN (
@Override
@Language("postgresql")
public String listMapTilesStatement() {
return """
SELECT t.x, t.z
FROM bluemap_map_tile t
INNER JOIN bluemap_map m
ON t.map = m.id
INNER JOIN bluemap_map_tile_compression c
ON t.compression = c.id
WHERE m.map_id = ?
AND t.lod = ?
AND c.compression = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("postgresql")
public String writeMapMetaStatement() {
return """
INSERT
INTO bluemap_map_meta (map, key, value)
VALUES (?, ?, ?)
ON CONFLICT (map, key)
DO UPDATE SET
value = excluded.value
""";
}
@Override
@Language("postgresql")
public String readMapMetaStatement() {
return """
SELECT t.value
FROM bluemap_map_meta t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
AND t.key = ?
""";
}
@Override
@Language("postgresql")
public String deleteMapMetaStatement() {
return """
DELETE
FROM bluemap_map_meta
WHERE map = ?
AND key = ?
""";
}
@Override
@Language("postgresql")
public String hasMapMetaStatement() {
return """
SELECT COUNT(*) > 0
FROM bluemap_map_meta t
INNER JOIN bluemap_map m
ON t.map = m.id
WHERE m.map_id = ?
AND t.key = ?
""";
}
@Override
@Language("postgresql")
public String purgeMapTileTableStatement() {
return """
DELETE
FROM bluemap_map_tile
WHERE map = ?
""";
}
@Override
@Language("postgresql")
public String purgeMapMetaTableStatement() {
return """
DELETE
FROM bluemap_map_meta
WHERE map = ?
""";
}
@Override
@Language("postgresql")
public String deleteMapStatement() {
public String purgeMapStatement() {
return """
DELETE
FROM bluemap_map
WHERE map = ?
WHERE id = ?
""";
}
@ -336,8 +322,8 @@ INTO bluemap_map (map_id)
public String findCompressionKeyStatement() {
return """
SELECT id
FROM bluemap_map_tile_compression
WHERE compression = ?
FROM bluemap_compression
WHERE key = ?
""";
}
@ -346,7 +332,47 @@ public String findCompressionKeyStatement() {
public String createCompressionKeyStatement() {
return """
INSERT
INTO bluemap_map_tile_compression (compression)
INTO bluemap_compression (key)
VALUES (?)
""";
}
@Override
@Language("postgresql")
public String findItemStorageKeyStatement() {
return """
SELECT id
FROM bluemap_item_storage
WHERE key = ?
""";
}
@Override
@Language("postgresql")
public String createItemStorageKeyStatement() {
return """
INSERT
INTO bluemap_item_storage (key)
VALUES (?)
""";
}
@Override
@Language("postgresql")
public String findGridStorageKeyStatement() {
return """
SELECT id
FROM bluemap_grid_storage
WHERE key = ?
""";
}
@Override
@Language("postgresql")
public String createGridStorageKeyStatement() {
return """
INSERT
INTO bluemap_grid_storage (key)
VALUES (?)
""";
}

View File

@ -48,51 +48,47 @@ public String createMapTableStatement() {
@Language("sqlite")
public String createCompressionTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile_compression` (
CREATE TABLE IF NOT EXISTS `bluemap_compression` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`compression` TEXT UNIQUE NOT NULL
`key` TEXT UNIQUE NOT NULL
) STRICT
""";
}
@Override
@Language("sqlite")
public String createMapMetaTableStatement() {
public String createItemStorageTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_meta` (
`map` INTEGER NOT NULL,
`key` TEXT NOT NULL,
`value` BLOB NOT NULL,
PRIMARY KEY (`map`, `key`),
CONSTRAINT `fk_bluemap_map_meta_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
CREATE TABLE IF NOT EXISTS `bluemap_item_storage` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`key` TEXT UNIQUE NOT NULL
) STRICT
""";
}
@Override
@Language("sqlite")
public String createMapTileTableStatement() {
public String createItemStorageDataTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_map_tile` (
CREATE TABLE IF NOT EXISTS `bluemap_item_storage_data` (
`map` INTEGER NOT NULL,
`lod` INTEGER NOT NULL,
`x` INTEGER NOT NULL,
`z` INTEGER NOT NULL,
`storage` INTEGER NOT NULL,
`compression` INTEGER NOT NULL,
`data` BLOB NOT NULL,
PRIMARY KEY (`map`, `lod`, `x`, `z`),
CONSTRAINT `fk_bluemap_map_tile_map`
PRIMARY KEY (`map`, `storage`),
CONSTRAINT `fk_bluemap_item_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_map_tile_compression`
CONSTRAINT `fk_bluemap_item`
FOREIGN KEY (`storage`)
REFERENCES `bluemap_item_storage` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_item_compression`
FOREIGN KEY (`compression`)
REFERENCES `bluemap_map_tile_compression` (`id`)
REFERENCES `bluemap_compression` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) STRICT
@ -101,50 +97,110 @@ FOREIGN KEY (`compression`)
@Override
@Language("sqlite")
public String fixLegacyCompressionIdsStatement() {
public String createGridStorageTableStatement() {
return """
UPDATE `bluemap_map_tile_compression`
SET `compression` = 'bluemap:' || `compression`
WHERE NOT `compression` LIKE '%:%'
CREATE TABLE IF NOT EXISTS `bluemap_grid_storage` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`key` TEXT UNIQUE NOT NULL
) STRICT
""";
}
@Override
@Language("sqlite")
public String writeMapTileStatement() {
public String createGridStorageDataTableStatement() {
return """
CREATE TABLE IF NOT EXISTS `bluemap_grid_storage_data` (
`map` INTEGER NOT NULL,
`storage` INTEGER NOT NULL,
`x` INTEGER NOT NULL,
`z` INTEGER NOT NULL,
`compression` INTEGER NOT NULL,
`data` BLOB NOT NULL,
PRIMARY KEY (`map`, `storage`, `x`, `z`),
CONSTRAINT `fk_bluemap_grid_map`
FOREIGN KEY (`map`)
REFERENCES `bluemap_map` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_grid`
FOREIGN KEY (`storage`)
REFERENCES `bluemap_grid_storage` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE,
CONSTRAINT `fk_bluemap_grid_compression`
FOREIGN KEY (`compression`)
REFERENCES `bluemap_compression` (`id`)
ON UPDATE RESTRICT
ON DELETE CASCADE
) STRICT
""";
}
@Override
@Language("sqlite")
public String itemStorageWriteStatement() {
return """
REPLACE
INTO `bluemap_map_tile` (`map`, `lod`, `x`, `z`, `compression`, `data`)
INTO `bluemap_item_storage_data` (`map`, `storage`, `compression`, `data`)
VALUES (?, ?, ?, ?)
""";
}
@Override
@Language("sqlite")
public String itemStorageReadStatement() {
return """
SELECT `data`
FROM `bluemap_item_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `compression` = ?
""";
}
@Override
@Language("sqlite")
public String itemStorageDeleteStatement() {
return """
DELETE
FROM `bluemap_item_storage_data`
WHERE `map` = ?
AND `storage` = ?
""";
}
@Override
@Language("sqlite")
public String itemStorageHasStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_item_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `compression` = ?
""";
}
@Override
@Language("sqlite")
public String gridStorageWriteStatement() {
return """
REPLACE
INTO `bluemap_grid_storage_data` (`map`, `storage`, `x`, `z`, `compression`, `data`)
VALUES (?, ?, ?, ?, ?, ?)
""";
}
@Override
@Language("sqlite")
public String readMapTileStatement() {
public String gridStorageReadStatement() {
return """
SELECT t.`data`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("sqlite")
public String deleteMapTileStatement() {
return """
DELETE
FROM `bluemap_map_tile`
SELECT `data`
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `lod` = ?
AND `storage` = ?
AND `x` = ?
AND `z` = ?
AND `compression` = ?
@ -153,44 +209,64 @@ public String deleteMapTileStatement() {
@Override
@Language("sqlite")
public String hasMapTileStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND t.`x` = ?
AND t.`z` = ?
AND c.`compression` = ?
""";
}
@Override
@Language("sqlite")
public String countAllMapTilesStatement() {
return """
SELECT COUNT(*)
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
""";
}
@Override
@Language("sqlite")
public String purgeMapTilesStatement() {
public String gridStorageDeleteStatement() {
return """
DELETE
FROM bluemap_map_tile
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `x` = ?
AND `z` = ?
""";
}
@Override
@Language("sqlite")
public String gridStorageHasStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `x` = ?
AND `z` = ?
AND `compression` = ?
""";
}
@Override
@Language("sqlite")
public String gridStorageListStatement() {
return """
SELECT `x`, `z`
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
AND `storage` = ?
AND `compression` = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("sqlite")
public String gridStorageCountMapItemsStatement() {
return """
SELECT COUNT(*)
FROM `bluemap_grid_storage_data`
WHERE `map` = ?
""";
}
@Override
@Language("sqlite")
public String gridStoragePurgeMapStatement() {
return """
DELETE
FROM `bluemap_grid_storage_data`
WHERE ROWID IN (
SELECT t.ROWID
FROM bluemap_map_tile t
WHERE t.map = ?
FROM `bluemap_grid_storage_data` t
WHERE t.`map` = ?
LIMIT ?
)
""";
@ -198,95 +274,11 @@ WHERE ROWID IN (
@Override
@Language("sqlite")
public String listMapTilesStatement() {
return """
SELECT t.`x`, t.`z`
FROM `bluemap_map_tile` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
INNER JOIN `bluemap_map_tile_compression` c
ON t.`compression` = c.`id`
WHERE m.`map_id` = ?
AND t.`lod` = ?
AND c.`compression` = ?
LIMIT ? OFFSET ?
""";
}
@Override
@Language("sqlite")
public String writeMapMetaStatement() {
return """
REPLACE
INTO `bluemap_map_meta` (`map`, `key`, `value`)
VALUES (?, ?, ?)
""";
}
@Override
@Language("sqlite")
public String readMapMetaStatement() {
return """
SELECT t.`value`
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
""";
}
@Override
@Language("sqlite")
public String deleteMapMetaStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` = ?
AND `key` = ?
""";
}
@Override
@Language("sqlite")
public String hasMapMetaStatement() {
return """
SELECT COUNT(*) > 0
FROM `bluemap_map_meta` t
INNER JOIN `bluemap_map` m
ON t.`map` = m.`id`
WHERE m.`map_id` = ?
AND t.`key` = ?
""";
}
@Override
@Language("sqlite")
public String purgeMapTileTableStatement() {
return """
DELETE
FROM `bluemap_map_tile`
WHERE `map` = ?
""";
}
@Override
@Language("sqlite")
public String purgeMapMetaTableStatement() {
return """
DELETE
FROM `bluemap_map_meta`
WHERE `map` = ?
""";
}
@Override
@Language("sqlite")
public String deleteMapStatement() {
public String purgeMapStatement() {
return """
DELETE
FROM `bluemap_map`
WHERE `map` = ?
WHERE `id` = ?
""";
}
@ -335,8 +327,8 @@ public String createMapKeyStatement() {
public String findCompressionKeyStatement() {
return """
SELECT `id`
FROM `bluemap_map_tile_compression`
WHERE `compression` = ?
FROM `bluemap_compression`
WHERE `key` = ?
""";
}
@ -345,7 +337,47 @@ public String findCompressionKeyStatement() {
public String createCompressionKeyStatement() {
return """
INSERT
INTO `bluemap_map_tile_compression` (`compression`)
INTO `bluemap_compression` (`key`)
VALUES (?)
""";
}
@Override
@Language("sqlite")
public String findItemStorageKeyStatement() {
return """
SELECT `id`
FROM `bluemap_item_storage`
WHERE `key` = ?
""";
}
@Override
@Language("sqlite")
public String createItemStorageKeyStatement() {
return """
INSERT
INTO `bluemap_item_storage` (`key`)
VALUES (?)
""";
}
@Override
@Language("sqlite")
public String findGridStorageKeyStatement() {
return """
SELECT `id`
FROM `bluemap_grid_storage`
WHERE `key` = ?
""";
}
@Override
@Language("sqlite")
public String createGridStorageKeyStatement() {
return """
INSERT
INTO `bluemap_grid_storage` (`key`)
VALUES (?)
""";
}

View File

@ -0,0 +1,8 @@
package de.bluecolored.bluemap.core.util;
@FunctionalInterface
public interface BiIntConsumer {
void accept(int a, int b);
}

View File

@ -39,12 +39,13 @@ public class FileHelper {
* once the stream gets closed.
*/
public static OutputStream createFilepartOutputStream(final Path file) throws IOException {
final Path partFile = getPartFile(file);
FileHelper.createDirectories(partFile.getParent());
Path folder = file.toAbsolutePath().normalize().getParent();
final Path partFile = folder.resolve(file.getFileName() + ".filepart");
FileHelper.createDirectories(folder);
OutputStream os = Files.newOutputStream(partFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
return new OnCloseOutputStream(os, () -> {
if (!Files.exists(partFile)) return;
FileHelper.createDirectories(file.getParent());
FileHelper.createDirectories(folder);
FileHelper.move(partFile, file);
});
}
@ -76,8 +77,4 @@ public static Path createDirectories(Path dir, FileAttribute<?>... attrs) throws
return Files.createDirectories(dir, attrs);
}
private static Path getPartFile(Path file) {
return file.normalize().getParent().resolve(file.getFileName() + ".filepart");
}
}

View File

@ -30,10 +30,24 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Consumer;
public class Grid {
public static final Grid UNIT = new Grid(Vector2i.ONE);
public static final Grid UNIT = new Grid(Vector2i.ONE, Vector2i.ZERO) {
@Override public int getCellX(int posX) { return posX; }
@Override public int getCellY(int posY) { return posY; }
@Override public Vector2i getCell(Vector2i pos) { return pos; }
@Override public int getLocalX(int posX) { return 0; }
@Override public int getLocalY(int posY) { return 0; }
@Override public Vector2i getLocal(Vector2i pos) { return pos; }
@Override public int getCellMinX(int cellX) { return cellX; }
@Override public int getCellMinY(int cellY) { return cellY; }
@Override public Vector2i getCellMin(Vector2i cell) { return cell; }
@Override public int getCellMaxX(int cellX) { return cellX; }
@Override public int getCellMaxY(int cellY) { return cellY; }
@Override public Vector2i getCellMax(Vector2i cell) { return cell; }
};
private final Vector2i gridSize;
private final Vector2i offset;
@ -158,13 +172,27 @@ public Vector2i getCellMax(Vector2i cell, Grid targetGrid) {
);
}
public void forEachIntersecting(Vector2i cell, Grid targetGrid, Consumer<Vector2i> action) {
forEachIntersecting(cell, targetGrid, (x, y) -> action.accept(new Vector2i(x, y)));
}
public void forEachIntersecting(Vector2i cell, Grid targetGrid, BiIntConsumer action) {
Vector2i min = getCellMin(cell, targetGrid);
Vector2i max = getCellMax(cell, targetGrid);
for (int x = min.getX(); x <= max.getX(); x++){
for (int y = min.getY(); y <= max.getY(); y++){
action.accept(x, y);
}
}
}
public Collection<Vector2i> getIntersecting(Vector2i cell, Grid targetGrid) {
Vector2i min = getCellMin(cell, targetGrid);
Vector2i max = getCellMax(cell, targetGrid);
if (min.equals(max)) return Collections.singleton(min);
Collection<Vector2i> intersects = new ArrayList<>();
Collection<Vector2i> intersects = new ArrayList<>((max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1));
for (int x = min.getX(); x <= max.getX(); x++){
for (int y = min.getY(); y <= max.getY(); y++){
intersects.add(new Vector2i(x, y));

View File

@ -34,7 +34,7 @@ public class Lazy<T> {
private Supplier<T> loader;
@DebugDump
private T value;
private volatile T value;
public Lazy(Supplier<T> loader) {
Objects.requireNonNull(loader);
@ -51,10 +51,14 @@ public Lazy(T value) {
}
public T getValue() {
if (!isLoaded()) {
if (value == null) {
synchronized (this) {
if (value == null) {
this.value = loader.get();
this.loader = null;
}
}
}
return this.value;
}

View File

@ -0,0 +1,75 @@
package de.bluecolored.bluemap.core.util;
import com.google.gson.reflect.TypeToken;
import de.bluecolored.bluenbt.BlueNBT;
import de.bluecolored.bluenbt.NBTReader;
import de.bluecolored.bluenbt.NBTWriter;
import de.bluecolored.bluenbt.TypeAdapter;
import de.bluecolored.bluenbt.adapter.ArrayAdapterFactory;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.HashMap;
@RequiredArgsConstructor
public class PalettedArrayAdapter<T> implements TypeAdapter<T[]> {
private final Class<T> type;
private final TypeAdapter<T[]> paletteAdapter;
@SuppressWarnings("unchecked")
public PalettedArrayAdapter(BlueNBT blueNBT, Class<T> type) {
this.type = type;
this.paletteAdapter = ArrayAdapterFactory.INSTANCE.create((TypeToken<T[]>) TypeToken.getArray(type), blueNBT).orElseThrow();
}
@SuppressWarnings("unchecked")
@Override
public T[] read(NBTReader reader) throws IOException {
reader.beginCompound();
T[] palette = null;
byte[] data = null;
while (reader.hasNext()) {
String name = reader.name();
switch (name) {
case "palette" -> palette = paletteAdapter.read(reader);
case "data" -> data = reader.nextArrayAsByteArray();
default -> reader.skip();
}
}
reader.endCompound();
if (palette == null || palette.length == 0) throw new IOException("Missing or empty palette");
if (data == null) return (T[]) Array.newInstance(type, 0);
T[] result = (T[]) Array.newInstance(type, data.length);
for (int i = 0; i < data.length; i++) {
byte index = data[i];
if (index >= palette.length) throw new IOException("Palette (size: " + palette.length + ") does not contain entry-index (" + index + ")");
result[i] = palette[data[i]];
}
return result;
}
@SuppressWarnings("unchecked")
@Override
public void write(T[] value, NBTWriter writer) throws IOException {
HashMap<T, Byte> paletteMap = new HashMap<>();
byte[] data = new byte[value.length];
for (int i = 0; i < value.length; i++) {
byte index = paletteMap.computeIfAbsent(value[i], v -> (byte) paletteMap.size());
data[i] = index;
}
T[] palette = (T[]) Array.newInstance(type, paletteMap.size());
paletteMap.forEach((k, v) -> palette[v] = k);
writer.beginCompound();
writer.name("palette");
paletteAdapter.write(palette, writer);
writer.name("data").value(data);
writer.endCompound();
}
}

View File

@ -24,26 +24,23 @@
*/
package de.bluecolored.bluemap.core.util;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@NoArgsConstructor
public class Registry<T extends Keyed> {
private final ConcurrentHashMap<Key, T> entries;
private final ConcurrentHashMap<Key, T> entries = new ConcurrentHashMap<>();
public Registry() {
this.entries = new ConcurrentHashMap<>();
}
private final Set<Key> keys = Collections.unmodifiableSet(entries.keySet());
private final Collection<T> values = Collections.unmodifiableCollection(entries.values());
@SafeVarargs
public Registry(T... defaultEntires) {
this();
for (T entry : defaultEntires)
public Registry(T... defaultEntries) {
for (T entry : defaultEntries)
register(entry);
}
@ -71,14 +68,14 @@ public boolean register(T entry) {
* Returns an unmodifiable set of all keys this registry contains entries for
*/
public Set<Key> keys() {
return Collections.unmodifiableSet(entries.keySet());
return keys;
}
/**
* Returns an unmodifiable collection of entries in this registry
*/
public Collection<T> values() {
return Collections.unmodifiableCollection(entries.values());
return values;
}
}

View File

@ -0,0 +1,39 @@
package de.bluecolored.bluemap.core.util;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluenbt.NBTReader;
import de.bluecolored.bluenbt.NBTWriter;
import de.bluecolored.bluenbt.TagType;
import de.bluecolored.bluenbt.TypeAdapter;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
@RequiredArgsConstructor
public class RegistryAdapter<T extends Keyed> implements TypeAdapter<T> {
private final Registry<T> registry;
private final String defaultNamespace;
private final T fallback;
@Override
public T read(NBTReader reader) throws IOException {
Key key = Key.parse(reader.nextString(), defaultNamespace);
T value = registry.get(key);
if (value != null) return value;
Logger.global.noFloodWarning("unknown-registry-key-" + key.getFormatted(), "Failed to find registry-entry for key: " + key);
return fallback;
}
@Override
public void write(T value, NBTWriter writer) throws IOException {
writer.value(value.getKey().getFormatted());
}
@Override
public TagType type() {
return TagType.STRING;
}
}

View File

@ -26,69 +26,35 @@
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.util.Key;
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.math.Color;
import lombok.Getter;
@DebugDump
public class Biome extends Key {
public interface Biome extends Keyed {
public static final Biome DEFAULT = new Biome("minecraft:ocean");
Biome DEFAULT = new Default();
private float humidity = 0.5f;
private float temp = 0.5f;
float getDownfall();
float getTemperature();
Color getWaterColor();
Color getOverlayFoliageColor();
Color getOverlayGrassColor();
@Getter
class Default implements Biome {
private final Key key = Key.bluemap("default");
private final float downfall = 0.5f;
private final float temperature = 0.5f;
private final Color waterColor = new Color().set(4159204 | 0xFF000000).premultiplied();
private final Color overlayFoliageColor = new Color().premultiplied();
private final Color overlayGrassColor = new Color().premultiplied();
public Biome(String formatted) {
super(formatted);
}
public Biome(String formatted, float humidity, float temp, Color waterColor) {
this(formatted);
this.humidity = humidity;
this.temp = temp;
this.waterColor.set(waterColor).premultiplied();
}
public Biome(String formatted, float humidity, float temp, Color waterColor, Color overlayFoliageColor, Color overlayGrassColor) {
this(formatted, humidity, temp, waterColor);
this.overlayFoliageColor.set(overlayFoliageColor).premultiplied();
this.overlayGrassColor.set(overlayGrassColor).premultiplied();
}
public float getHumidity() {
return humidity;
}
public float getTemp() {
return temp;
}
public Color getWaterColor() {
return waterColor;
}
public Color getOverlayFoliageColor() {
return overlayFoliageColor;
}
public Color getOverlayGrassColor() {
return overlayGrassColor;
}
@Override
public String toString() {
return "Biome{" +
"value='" + getValue() + '\'' +
", namespace=" + getNamespace() +
", formatted=" + getFormatted() +
", humidity=" + humidity +
", temp=" + temp +
", waterColor=" + waterColor +
", overlayFoliageColor=" + overlayFoliageColor +
", overlayGrassColor=" + overlayGrassColor +
'}';
}
}

View File

@ -30,6 +30,7 @@
public interface Chunk {
Chunk EMPTY_CHUNK = new Chunk() {};
Chunk ERRORED_CHUNK = new Chunk() {};
default boolean isGenerated() {
return false;
@ -51,8 +52,8 @@ default LightData getLightData(int x, int y, int z, LightData target) {
return target.set(0, 0);
}
default String getBiome(int x, int y, int z) {
return Biome.DEFAULT.getFormatted();
default Biome getBiome(int x, int y, int z) {
return Biome.DEFAULT;
}
default int getMaxY(int x, int z) {
@ -75,5 +76,6 @@ default boolean hasOceanFloorHeights() {
default int getOceanFloorY(int x, int z) { return 0; }
default @Nullable BlockEntity getBlockEntity(int x, int y, int z) { return null; };
default @Nullable BlockEntity getBlockEntity(int x, int y, int z) { return null; }
}

View File

@ -27,7 +27,7 @@
@FunctionalInterface
public interface ChunkConsumer {
default boolean filter(int chunkX, int chunkZ, long lastModified) {
default boolean filter(int chunkX, int chunkZ, int lastModified) {
return true;
}
@ -36,10 +36,10 @@ default boolean filter(int chunkX, int chunkZ, long lastModified) {
@FunctionalInterface
interface ListOnly extends ChunkConsumer {
void accept(int chunkX, int chunkZ, long lastModified);
void accept(int chunkX, int chunkZ, int lastModified);
@Override
default boolean filter(int chunkX, int chunkZ, long lastModified) {
default boolean filter(int chunkX, int chunkZ, int lastModified) {
accept(chunkX, chunkZ, lastModified);
return false;
}

View File

@ -37,7 +37,7 @@ class SingleChunkConsumer implements ChunkConsumer {
private Chunk foundChunk = Chunk.EMPTY_CHUNK;
@Override
public boolean filter(int x, int z, long lastModified) {
public boolean filter(int x, int z, int lastModified) {
return x == chunkX && z == chunkZ;
}
@ -53,7 +53,7 @@ public void accept(int chunkX, int chunkZ, Chunk chunk) {
}
/**
* Iterates over all chunks in this region and first calls {@link ChunkConsumer#filter(int, int, long)}.<br>
* Iterates over all chunks in this region and first calls {@link ChunkConsumer#filter(int, int, int)}.<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

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