update to Paper 26.3: load biome colors from vanilla data, fix dappled forest grass
Biome grass/foliage/dry-foliage/water colors and grass color modifiers are now read from the server's built-in worldgen biome data at runtime, with a 26.3 snapshot as fallback. Fixes missing biomes (dappled_forest, pale_garden, sulfur_caves) and several outdated values. Replace deprecated ExactChoice constructors.
This commit is contained in:
1 parent
8dd295e5d6
commit
2d84a35cc4
4 files changed
+168
-100
No files matched your search
@@ -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.
|
||||
*
|
||||
* <p>The definitions are read at runtime from the server's built-in data pack
|
||||
* ({@code data/minecraft/worldgen/biome/<name>.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<String, Climate> 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<String, Climate> FALLBACK = new HashMap<>();
|
||||
private static final Map<String, Optional<Climate>> 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<Climate> loaded = LOADED.computeIfAbsent(biomePath, BiomeClimate::loadFromServerData);
|
||||
return loaded.orElseGet(() -> FALLBACK.getOrDefault(biomePath, DEFAULT));
|
||||
}
|
||||
|
||||
private static Optional<Climate> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user