diff --git a/build.gradle b/build.gradle index 489147f..c5016ea 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compileOnly("io.papermc.paper:paper-api:26.2.build.129-stable") + compileOnly("io.papermc.paper:paper-api:26.3.build.240-stable") } def targetJavaVersion = 25 diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java index 0905b5f..ad51ad0 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeClimate.java @@ -1,100 +1,171 @@ package eu.mhsl.minecraft.pixelpics.render.tint; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; /** - * Hardcoded vanilla temperature/downfall and water color per biome. Used to drive the colormap - * lookup, since Paper does not expose the client-side per-block climate reliably. Unknown biomes - * fall back to a plains-like default. + * Per-biome climate (temperature/downfall) and color effects (water, grass/foliage/dry-foliage + * overrides, grass color modifier) as defined by the vanilla biome data. + * + *

The definitions are read at runtime from the server's built-in data pack + * ({@code data/minecraft/worldgen/biome/.json} on the server classpath), so new or changed + * vanilla biomes are picked up automatically after a server update. If that lookup fails, a + * snapshot of the vanilla 26.3 values is used; unknown biomes fall back to a plains-like default. */ public final class BiomeClimate { - public record Climate(double temperature, double downfall, int water) {} + /** Marker for "no explicit color" (use the colormap). */ + public static final int NONE = -1; + + /** + * @param water ARGB-less RGB water color + * @param grass explicit grass color override, or {@link #NONE} + * @param foliage explicit foliage color override, or {@link #NONE} + * @param dryFoliage explicit dry-foliage color override, or {@link #NONE} + * @param grassModifier vanilla {@code grass_color_modifier} ("swamp", "dark_forest") or null + */ + public record Climate( + double temperature, + double downfall, + int water, + int grass, + int foliage, + int dryFoliage, + String grassModifier) {} public static final int DEFAULT_WATER = 0x3F76E4; - public static final Climate DEFAULT = new Climate(0.8, 0.4, DEFAULT_WATER); + public static final Climate DEFAULT = new Climate(0.8, 0.4, DEFAULT_WATER, NONE, NONE, NONE, null); - private static final Map TABLE = new HashMap<>(); + private static final String DATA_PATH = "data/minecraft/worldgen/biome/"; - private static void put(String key, double t, double d) { - TABLE.put(key, new Climate(t, d, DEFAULT_WATER)); - } - - private static void put(String key, double t, double d, int water) { - TABLE.put(key, new Climate(t, d, water)); + private static final Map FALLBACK = new HashMap<>(); + private static final Map> LOADED = new ConcurrentHashMap<>(); + + private static void put( + String key, double t, double d, int water, int grass, int foliage, int dry, String modifier) { + FALLBACK.put(key, new Climate(t, d, water, grass, foliage, dry, modifier)); } + // Snapshot of vanilla 26.3 data/minecraft/worldgen/biome/*.json static { - put("plains", 0.8, 0.4); - put("sunflower_plains", 0.8, 0.4); - put("snowy_plains", 0.0, 0.5); - put("ice_spikes", 0.0, 0.5); - put("desert", 2.0, 0.0); - put("swamp", 0.8, 0.9, 0x617B64); - put("mangrove_swamp", 0.8, 0.9, 0x3A7A6A); - put("forest", 0.7, 0.8); - put("flower_forest", 0.7, 0.8); - put("birch_forest", 0.6, 0.6); - put("old_growth_birch_forest", 0.6, 0.6); - put("dark_forest", 0.7, 0.8); - put("old_growth_pine_taiga", 0.3, 0.8); - put("old_growth_spruce_taiga", 0.25, 0.8); - put("taiga", 0.25, 0.8); - put("snowy_taiga", -0.5, 0.4); - put("savanna", 2.0, 0.0); - put("savanna_plateau", 2.0, 0.0); - put("windswept_hills", 0.2, 0.3); - put("windswept_gravelly_hills", 0.2, 0.3); - put("windswept_forest", 0.2, 0.3); - put("windswept_savanna", 2.0, 0.0); - put("jungle", 0.95, 0.9); - put("sparse_jungle", 0.95, 0.8); - put("bamboo_jungle", 0.95, 0.9); - put("badlands", 2.0, 0.0); - put("eroded_badlands", 2.0, 0.0); - put("wooded_badlands", 2.0, 0.0); - put("meadow", 0.5, 0.8); - put("cherry_grove", 0.5, 0.8, 0x5DB7DD); - put("grove", -0.2, 0.8); - put("snowy_slopes", -0.3, 0.9); - put("frozen_peaks", -0.7, 0.9); - put("jagged_peaks", -0.7, 0.9); - put("stony_peaks", 1.0, 0.3); - put("river", 0.5, 0.5); - put("frozen_river", 0.0, 0.5, 0x3938C9); - put("beach", 0.8, 0.4); - put("snowy_beach", 0.05, 0.3, 0x3D57D6); - put("stony_shore", 0.2, 0.3); - put("warm_ocean", 0.5, 0.5, 0x43D5EE); - put("lukewarm_ocean", 0.5, 0.5, 0x45ADF2); - put("deep_lukewarm_ocean", 0.5, 0.5, 0x45ADF2); - put("ocean", 0.5, 0.5); - put("deep_ocean", 0.5, 0.5); - put("cold_ocean", 0.5, 0.5, 0x3D57D6); - put("deep_cold_ocean", 0.5, 0.5, 0x3D57D6); - put("frozen_ocean", 0.0, 0.5, 0x3938C9); - put("deep_frozen_ocean", 0.5, 0.5, 0x3938C9); - put("mushroom_fields", 0.9, 1.0); - put("dripstone_caves", 0.8, 0.4); - put("lush_caves", 0.5, 0.5); - put("deep_dark", 0.8, 0.4); - put("nether_wastes", 2.0, 0.0, 0x905957); - put("soul_sand_valley", 2.0, 0.0, 0x905957); - put("crimson_forest", 2.0, 0.0, 0x905957); - put("warped_forest", 2.0, 0.0, 0x905957); - put("basalt_deltas", 2.0, 0.0, 0x3F76E4); - put("the_end", 0.5, 0.5, 0x62529E); - put("end_highlands", 0.5, 0.5, 0x62529E); - put("end_midlands", 0.5, 0.5, 0x62529E); - put("small_end_islands", 0.5, 0.5, 0x62529E); - put("end_barrens", 0.5, 0.5, 0x62529E); - put("the_void", 0.5, 0.5); + put("badlands", 2.0, 0.0, 0x3F76E4, 0x90814D, 0x9E814D, NONE, null); + put("bamboo_jungle", 0.95, 0.9, 0x3F76E4, NONE, NONE, NONE, null); + put("basalt_deltas", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("beach", 0.8, 0.4, 0x3F76E4, NONE, NONE, NONE, null); + put("birch_forest", 0.6, 0.6, 0x3F76E4, NONE, NONE, NONE, null); + put("cherry_grove", 0.5, 0.8, 0x5DB7EF, 0xB6DB61, 0xB6DB61, NONE, null); + put("cold_ocean", 0.5, 0.5, 0x3D57D6, NONE, NONE, NONE, null); + put("crimson_forest", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("dappled_forest", 0.6, 0.6, 0x375154, 0xDF6827, 0xE68E30, 0x8C3A04, null); + put("dark_forest", 0.7, 0.8, 0x3F76E4, NONE, NONE, 0x7B5334, "dark_forest"); + put("deep_cold_ocean", 0.5, 0.5, 0x3D57D6, NONE, NONE, NONE, null); + put("deep_dark", 0.8, 0.4, 0x3F76E4, NONE, NONE, NONE, null); + put("deep_frozen_ocean", 0.5, 0.5, 0x3938C9, NONE, NONE, NONE, null); + put("deep_lukewarm_ocean", 0.5, 0.5, 0x45ADF2, NONE, NONE, NONE, null); + put("deep_ocean", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("desert", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("dripstone_caves", 0.8, 0.4, 0x3F76E4, NONE, NONE, NONE, null); + put("end_barrens", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("end_highlands", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("end_midlands", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("eroded_badlands", 2.0, 0.0, 0x3F76E4, 0x90814D, 0x9E814D, NONE, null); + put("flower_forest", 0.7, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("forest", 0.7, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("frozen_ocean", 0.0, 0.5, 0x3938C9, NONE, NONE, NONE, null); + put("frozen_peaks", -0.7, 0.9, 0x3F76E4, NONE, NONE, NONE, null); + put("frozen_river", 0.0, 0.5, 0x3938C9, NONE, NONE, NONE, null); + put("grove", -0.2, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("ice_spikes", 0.0, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("jagged_peaks", -0.7, 0.9, 0x3F76E4, NONE, NONE, NONE, null); + put("jungle", 0.95, 0.9, 0x3F76E4, NONE, NONE, NONE, null); + put("lukewarm_ocean", 0.5, 0.5, 0x45ADF2, NONE, NONE, NONE, null); + put("lush_caves", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("mangrove_swamp", 0.8, 0.9, 0x3A7A6A, NONE, 0x8DB127, 0x7B5334, "swamp"); + put("meadow", 0.5, 0.8, 0x0E4ECF, NONE, NONE, NONE, null); + put("mushroom_fields", 0.9, 1.0, 0x3F76E4, NONE, NONE, NONE, null); + put("nether_wastes", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("ocean", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("old_growth_birch_forest", 0.6, 0.6, 0x3F76E4, NONE, NONE, NONE, null); + put("old_growth_pine_taiga", 0.3, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("old_growth_spruce_taiga", 0.25, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("pale_garden", 0.7, 0.8, 0x76889D, 0x778272, 0x878D76, 0xA0A69C, null); + put("plains", 0.8, 0.4, 0x3F76E4, NONE, NONE, NONE, null); + put("river", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("savanna", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("savanna_plateau", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("small_end_islands", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("snowy_beach", 0.05, 0.3, 0x3D57D6, NONE, NONE, NONE, null); + put("snowy_plains", 0.0, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("snowy_slopes", -0.3, 0.9, 0x3F76E4, NONE, NONE, NONE, null); + put("snowy_taiga", -0.5, 0.4, 0x3D57D6, NONE, NONE, NONE, null); + put("soul_sand_valley", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("sparse_jungle", 0.95, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("stony_peaks", 1.0, 0.3, 0x3F76E4, NONE, NONE, NONE, null); + put("stony_shore", 0.2, 0.3, 0x3F76E4, NONE, NONE, NONE, null); + put("sulfur_caves", 0.8, 0.4, 0x34BF89, 0xABA64F, NONE, NONE, null); + put("sunflower_plains", 0.8, 0.4, 0x3F76E4, NONE, NONE, NONE, null); + put("swamp", 0.8, 0.9, 0x617B64, NONE, 0x6A7039, 0x7B5334, "swamp"); + put("taiga", 0.25, 0.8, 0x3F76E4, NONE, NONE, NONE, null); + put("the_end", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("the_void", 0.5, 0.5, 0x3F76E4, NONE, NONE, NONE, null); + put("warm_ocean", 0.5, 0.5, 0x43D5EE, NONE, NONE, NONE, null); + put("warped_forest", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("windswept_forest", 0.2, 0.3, 0x3F76E4, NONE, NONE, NONE, null); + put("windswept_gravelly_hills", 0.2, 0.3, 0x3F76E4, NONE, NONE, NONE, null); + put("windswept_hills", 0.2, 0.3, 0x3F76E4, NONE, NONE, NONE, null); + put("windswept_savanna", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null); + put("wooded_badlands", 2.0, 0.0, 0x3F76E4, 0x90814D, 0x9E814D, NONE, null); } private BiomeClimate() {} public static Climate forKey(String biomePath) { - return TABLE.getOrDefault(biomePath, DEFAULT); + Optional loaded = LOADED.computeIfAbsent(biomePath, BiomeClimate::loadFromServerData); + return loaded.orElseGet(() -> FALLBACK.getOrDefault(biomePath, DEFAULT)); + } + + private static Optional loadFromServerData(String biomePath) { + if (!biomePath.matches("[a-z0-9_./-]+")) return Optional.empty(); + ClassLoader loader = BiomeClimate.class.getClassLoader(); + if (loader == null) return Optional.empty(); + try (InputStream in = loader.getResourceAsStream(DATA_PATH + biomePath + ".json")) { + if (in == null) return Optional.empty(); + JsonObject root = JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .getAsJsonObject(); + JsonObject effects = root.has("effects") ? root.getAsJsonObject("effects") : new JsonObject(); + JsonElement modifier = effects.get("grass_color_modifier"); + return Optional.of(new Climate( + root.has("temperature") ? root.get("temperature").getAsDouble() : DEFAULT.temperature(), + root.has("downfall") ? root.get("downfall").getAsDouble() : DEFAULT.downfall(), + color(effects, "water_color", DEFAULT_WATER), + color(effects, "grass_color", NONE), + color(effects, "foliage_color", NONE), + color(effects, "dry_foliage_color", NONE), + modifier != null && modifier.isJsonPrimitive() ? modifier.getAsString() : null)); + } catch (Exception e) { + return Optional.empty(); + } + } + + /** Colors are either "#rrggbb" strings or packed integers, depending on the data version. */ + private static int color(JsonObject effects, String key, int fallback) { + JsonElement el = effects.get(key); + if (el == null || !el.isJsonPrimitive()) return fallback; + if (el.getAsJsonPrimitive().isNumber()) return el.getAsInt() & 0xFFFFFF; + String s = el.getAsString().trim(); + if (s.startsWith("#")) s = s.substring(1); + try { + return Integer.parseInt(s, 16) & 0xFFFFFF; + } catch (NumberFormatException e) { + return fallback; + } } } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java index ca8d206..3444481 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/tint/BiomeTintProvider.java @@ -39,26 +39,23 @@ public final class BiomeTintProvider { private BiomeTint compute(String key) { BiomeClimate.Climate climate = BiomeClimate.forKey(key); - int grass = sample(grassMap, climate.temperature(), climate.downfall(), 0xFF91BD59); - int foliage = sample(foliageMap, climate.temperature(), climate.downfall(), 0xFF77AB2F); - int dry = sample(dryFoliageMap, climate.temperature(), climate.downfall(), 0xFFA9A05B); + // Explicit biome colors (biome "effects") win over the colormap, like in vanilla. + int grass = climate.grass() != BiomeClimate.NONE + ? 0xFF000000 | climate.grass() + : sample(grassMap, climate.temperature(), climate.downfall(), 0xFF91BD59); + int foliage = climate.foliage() != BiomeClimate.NONE + ? 0xFF000000 | climate.foliage() + : sample(foliageMap, climate.temperature(), climate.downfall(), 0xFF77AB2F); + int dry = climate.dryFoliage() != BiomeClimate.NONE + ? 0xFF000000 | climate.dryFoliage() + : sample(dryFoliageMap, climate.temperature(), climate.downfall(), 0xFFA9A05B); - // Vanilla per-biome grass/foliage color overrides and modifiers that the colormap alone misses. - switch (key) { - case "swamp", "mangrove_swamp" -> { - grass = 0xFF6A7039; - foliage = 0xFF6A7039; - } - case "badlands", "eroded_badlands", "wooded_badlands" -> { - grass = 0xFF90814D; - foliage = 0xFF9E814D; - } - case "dark_forest" -> { - // DARK_FOREST modifier: ((color & 0xFEFEFE) + 0x28340A) >> 1 - grass = 0xFF000000 | (((grass & 0xFEFEFE) + 0x28340A) >> 1); - foliage = 0xFF000000 | (((foliage & 0xFEFEFE) + 0x28340A) >> 1); - } - default -> {} + // Vanilla grass_color_modifier (applies to grass only). + if ("swamp".equals(climate.grassModifier())) { + // Vanilla picks 0x4C763C/0x6A7039 via position noise; use the dominant color. + grass = 0xFF6A7039; + } else if ("dark_forest".equals(climate.grassModifier())) { + grass = 0xFF000000 | (((grass & 0xFEFEFE) + 0x28340A) >> 1); } return new BiomeTint(grass, foliage, dry, 0xFF000000 | climate.water()); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java index 8025966..d199582 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/SurvivalRecipes.java @@ -63,8 +63,8 @@ public final class SurvivalRecipes { loadableCameras.add(CameraItems.createCamera(i)); } ShapelessRecipe load = new ShapelessRecipe(LOAD, CameraItems.createCamera(1)); - load.addIngredient(new RecipeChoice.ExactChoice(loadableCameras)); - load.addIngredient(new RecipeChoice.ExactChoice(CameraItems.createFilm())); + load.addIngredient(RecipeChoice.exactChoice(loadableCameras)); + load.addIngredient(RecipeChoice.exactChoice(CameraItems.createFilm())); Bukkit.addRecipe(load, false); // Copy photo: photo + film. The photo carries a variable map id (cannot be enumerated), so it @@ -72,7 +72,7 @@ public final class SurvivalRecipes { // its real texture shows in the book. ShapelessRecipe copy = new ShapelessRecipe(COPY, new ItemStack(Material.FILLED_MAP)); copy.addIngredient(new RecipeChoice.MaterialChoice(Material.FILLED_MAP)); - copy.addIngredient(new RecipeChoice.ExactChoice(CameraItems.createFilm())); + copy.addIngredient(RecipeChoice.exactChoice(CameraItems.createFilm())); Bukkit.addRecipe(copy, false); // Cover /reload while players are online; fresh joins are handled by JoinListener.