diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java index f309c31fd6..bbaf39e999 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -166,6 +166,7 @@ import org.bukkit.craftbukkit.inventory.CraftSmokingRecipe; import org.bukkit.craftbukkit.inventory.CraftStonecuttingRecipe; import org.bukkit.craftbukkit.inventory.RecipeIterator; import org.bukkit.craftbukkit.inventory.util.CraftInventoryCreator; +import org.bukkit.craftbukkit.map.CraftMapColorCache; import org.bukkit.craftbukkit.map.CraftMapView; import org.bukkit.craftbukkit.metadata.EntityMetadataStore; import org.bukkit.craftbukkit.metadata.PlayerMetadataStore; @@ -217,6 +218,7 @@ import org.bukkit.inventory.SmithingRecipe; import org.bukkit.inventory.SmokingRecipe; import org.bukkit.inventory.StonecuttingRecipe; import org.bukkit.loot.LootTable; +import org.bukkit.map.MapPalette; import org.bukkit.map.MapView; import org.bukkit.permissions.Permissible; import org.bukkit.permissions.Permission; @@ -353,6 +355,11 @@ public final class CraftServer implements Server { TicketType.PLUGIN.timeout = configuration.getInt("chunk-gc.period-in-ticks"); minimumAPI = configuration.getString("settings.minimum-api"); loadIcon(); + + // Set map color cache + if (configuration.getBoolean("settings.use-map-color-cache")) { + MapPalette.setMapColorCache(new CraftMapColorCache(logger)); + } } public boolean getCommandBlockOverride(String command) { diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/map/CraftMapColorCache.java b/paper-server/src/main/java/org/bukkit/craftbukkit/map/CraftMapColorCache.java new file mode 100644 index 0000000000..3970102943 --- /dev/null +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/map/CraftMapColorCache.java @@ -0,0 +1,162 @@ +package org.bukkit.craftbukkit.map; + +import com.google.common.base.Preconditions; +import java.awt.Color; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; +import net.minecraft.SystemUtils; +import org.bukkit.map.MapPalette; + +public class CraftMapColorCache implements MapPalette.MapColorCache { + + private static final String MD5_CACHE_HASH = "E88EDD068D12D39934B40E8B6B124C83"; + private static final File CACHE_FILE = new File("map-color-cache.dat"); + private byte[] cache; + private final Logger logger; + private boolean cached = false; + private boolean running = false; + + public CraftMapColorCache(Logger logger) { + this.logger = logger; + } + + // Builds and prints the md5 hash of the cache, this should be run when new map colors are added to update the MD5_CACHE_HASH string + public static void main(String[] args) { + CraftMapColorCache craftMapColorCache = new CraftMapColorCache(Logger.getGlobal()); + craftMapColorCache.buildCache(); + try { + byte[] hash = MessageDigest.getInstance("MD5").digest(craftMapColorCache.cache); + System.out.println("MD5_CACHE_HASH: " + bytesToString(hash)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + public static String bytesToString(byte[] bytes) { + char[] chars = "0123456789ABCDEF".toCharArray(); + + StringBuilder builder = new StringBuilder(); + for (byte value : bytes) { + int first = (value & 0xF0) >> 4; + int second = value & 0x0F; + builder.append(chars[first]); + builder.append(chars[second]); + } + + return builder.toString(); + } + + public CompletableFuture initCache() { + Preconditions.checkState(!cached && !running, "Cache is already build or is currently being build"); + + cache = new byte[256 * 256 * 256]; // Red, Green and Blue have each a range from 0 to 255 each mean we need space for 256 * 256 * 256 values + if (CACHE_FILE.exists()) { + byte[] fileContent; + + try (InputStream inputStream = new InflaterInputStream(new FileInputStream(CACHE_FILE))) { + fileContent = inputStream.readAllBytes(); + } catch (IOException e) { + logger.warning("Error while reading map color cache"); + e.printStackTrace(); + return CompletableFuture.completedFuture(null); + } + + byte[] hash; + try { + hash = MessageDigest.getInstance("MD5").digest(fileContent); + } catch (NoSuchAlgorithmException e) { + logger.warning("Error while hashing map color cache"); + e.printStackTrace(); + return CompletableFuture.completedFuture(null); + } + + if (!MD5_CACHE_HASH.equals(bytesToString(hash))) { + logger.info("Map color cache hash invalid, rebuilding cache in the background"); + return buildAndSaveCache(); + } else { + System.arraycopy(fileContent, 0, cache, 0, fileContent.length); + } + + cached = true; + } else { + logger.info("Map color cache not found, building it in the background"); + return buildAndSaveCache(); + } + + return CompletableFuture.completedFuture(null); + } + + private void buildCache() { + for (int r = 0; r < 256; r++) { + for (int g = 0; g < 256; g++) { + for (int b = 0; b < 256; b++) { + Color color = new Color(r, g, b); + cache[toInt(color)] = MapPalette.matchColor(color); + } + } + } + } + + private CompletableFuture buildAndSaveCache() { + running = true; + return CompletableFuture.runAsync(() -> { + buildCache(); + + if (!CACHE_FILE.exists()) { + try { + if (!CACHE_FILE.createNewFile()) { + running = false; + cached = true; + return; + } + } catch (IOException e) { + logger.warning("Error while building map color cache"); + e.printStackTrace(); + running = false; + cached = true; + return; + } + } + + try (OutputStream outputStream = new DeflaterOutputStream(new FileOutputStream(CACHE_FILE))) { + outputStream.write(cache); + } catch (IOException e) { + logger.warning("Error while building map color cache"); + e.printStackTrace(); + running = false; + cached = true; + return; + } + + running = false; + cached = true; + logger.info("Map color cache build successfully"); + }, SystemUtils.backgroundExecutor()); + } + + private int toInt(Color color) { + return color.getRGB() & 0xFFFFFF; + } + + @Override + public boolean isCached() { + return cached || (!running && initCache().isDone()); + } + + @Override + public byte matchColor(Color color) { + Preconditions.checkState(isCached(), "Cache not build jet"); + + return cache[toInt(color)]; + } +} diff --git a/paper-server/src/main/resources/configurations/bukkit.yml b/paper-server/src/main/resources/configurations/bukkit.yml index 97239f7aad..eef7c125b2 100644 --- a/paper-server/src/main/resources/configurations/bukkit.yml +++ b/paper-server/src/main/resources/configurations/bukkit.yml @@ -22,6 +22,7 @@ settings: deprecated-verbose: default shutdown-message: Server closed minimum-api: none + use-map-color-cache: true spawn-limits: monsters: 70 animals: 10 diff --git a/paper-server/src/test/java/org/bukkit/map/MapTest.java b/paper-server/src/test/java/org/bukkit/map/MapTest.java index ef7cc9b2b6..e47e7c009d 100644 --- a/paper-server/src/test/java/org/bukkit/map/MapTest.java +++ b/paper-server/src/test/java/org/bukkit/map/MapTest.java @@ -1,10 +1,13 @@ package org.bukkit.map; import java.awt.Color; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import net.minecraft.world.level.material.MaterialMapColor; +import org.bukkit.craftbukkit.map.CraftMapColorCache; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; public class MapTest { @@ -34,7 +37,7 @@ public class MapTest { int mr = (r * modi) / 255; int mg = (g * modi) / 255; int mb = (b * modi) / 255; - logger.log(Level.WARNING, "Missing color (check CraftMapView#render): c({0}, {1}, {2})", new Object[]{mr, mg, mb}); + logger.log(Level.WARNING, "Missing color (check CraftMapView#render and update md5 hash in CraftMapColorCache): c({0}, {1}, {2})", new Object[]{mr, mg, mb}); } fail = true; } else { @@ -58,4 +61,20 @@ public class MapTest { } Assert.assertFalse(fail); } + + @Ignore("Test takes around 25 seconds, should be run by changes to the map color conversion") + @Test + public void testMapColorCacheBuilding() throws ExecutionException, InterruptedException { + CraftMapColorCache craftMapColorCache = new CraftMapColorCache(logger); + craftMapColorCache.initCache().get(); + + for (int r = 0; r < 256; r++) { + for (int g = 0; g < 256; g++) { + for (int b = 0; b < 256; b++) { + Color color = new Color(r, g, b); + Assert.assertEquals(String.format("Incorrect matched color c(%s, %s, %s)", color.getRed(), color.getGreen(), color.getBlue()), MapPalette.matchColor(color), craftMapColorCache.matchColor(color)); + } + } + } + } }