Merge pull request #5897 from Vaspyyy/feat/native-biome-colors

🤖 Agent: Client hook native_biome_colors + native_refresh_biome_colors as an API fragment: runs only when a mod asks for a refresh (rebuilds sections like a Biome Blend change), nothing per frame; Biomes::with_colors unit-tested.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-01 15:03:56 +01:00
commit 885e287a2e
6 files changed
+203

No files matched your search

+121
View File
@@ -387,6 +387,52 @@ impl Biomes {
}
}
/// A biome's colours as a mod may change them (`native_biome_colors`), each
/// 0xRRGGBB: what tints grass (before the dark forest's modifier; the swamp's
/// noise colours replace it), leaves and vines, dry foliage (leaf litter) and
/// water.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BiomeColors {
pub grass: u32,
pub foliage: u32,
pub dry_foliage: u32,
pub water: u32,
}
impl Biomes {
/// This table with each biome's colours passed through `f`, given the
/// biome's name (`minecraft:plains`) and its colours; None when `f`
/// changed none. Bits past 0xFFFFFF are dropped.
pub fn with_colors(&self, mut f: impl FnMut(&str, &mut BiomeColors)) -> Option<Self> {
let mut out = self.clone();
let mut changed = false;
for b in &mut out.list {
let before = BiomeColors {
grass: b.grass,
foliage: b.foliage,
dry_foliage: b.dry_foliage,
water: b.water,
};
let mut colors = before;
f(&b.name, &mut colors);
let after = BiomeColors {
grass: colors.grass & 0xFF_FFFF,
foliage: colors.foliage & 0xFF_FFFF,
dry_foliage: colors.dry_foliage & 0xFF_FFFF,
water: colors.water & 0xFF_FFFF,
};
if after != before {
changed = true;
b.grass = after.grass;
b.foliage = after.foliage;
b.dry_foliage = after.dry_foliage;
b.water = after.water;
}
}
changed.then_some(out)
}
}
/// A 24-bit colour as sRGB components in 0..1.
pub fn rgb(c: u32) -> [f32; 3] {
[16, 8, 0].map(|shift| (c >> shift & 255) as f32 / 255.0)
@@ -534,6 +580,81 @@ mod pack_tests {
}
}
#[cfg(test)]
mod color_tests {
use super::*;
use serde_json::json;
fn biomes() -> Biomes {
let maps = Colormaps {
grass: Vec::new(),
foliage: Vec::new(),
dry: Vec::new(),
};
let effects = |grass: u32, modifier: &str| {
json!({"effects": {"grass_color": grass, "foliage_color": 0x112233,
"dry_foliage_color": 0x445566, "water_color": 0x3F76E4,
"grass_color_modifier": modifier}})
};
let list = vec![
BiomeInfo::from_json("minecraft:plains".into(), &effects(0x91BD59, "none"), &maps),
BiomeInfo::from_json(
"minecraft:dark_forest".into(),
&effects(0x91BD59, "dark_forest"),
&maps,
),
];
Biomes {
builtin: Arc::new(list.clone()),
index: list
.iter()
.enumerate()
.map(|(i, b)| (b.name.clone(), i as u16))
.collect(),
list,
info_noise: SimplexNoise::with_skipped_offsets(&mut LegacyRandom::new(2345), true),
colormaps: Arc::new(maps),
packs: BTreeMap::new(),
}
}
#[test]
fn a_change_reaches_the_tints_the_mesher_reads() {
let autumn = biomes()
.with_colors(|name, c| {
c.foliage = 0xD2691E;
if name == "minecraft:plains" {
c.grass = 0xBFA055;
}
})
.expect("changed");
let plains = autumn.id("minecraft:plains");
assert_eq!(autumn.grass(plains, 0.0, 0.0), 0xBFA055);
assert_eq!(autumn.get(plains).foliage, 0xD2691E);
assert_eq!(autumn.get(plains).dry_foliage, 0x445566);
assert_eq!(autumn.get(plains).water, 0x3F76E4);
// The dark forest's modifier still darkens its (unchanged) grass:
// ((0x91BD59 & 0xFEFEFE) + 0x28340A) >> 1.
let dark = autumn.id("minecraft:dark_forest");
assert_eq!(autumn.grass(dark, 0.0, 0.0), 0x5C7831);
assert_eq!(autumn.get(dark).foliage, 0xD2691E);
}
#[test]
fn no_change_gives_none_and_alpha_bits_are_dropped() {
assert!(biomes().with_colors(|_, _| {}).is_none());
assert!(
biomes()
.with_colors(|_, c| c.water |= 0xFF00_0000)
.is_none()
);
let blue = biomes()
.with_colors(|_, c| c.water = 0xFF00_00FF)
.expect("changed");
assert_eq!(blue.get(0).water, 0x0000FF);
}
}
#[cfg(test)]
mod blend_tests {
use super::*;
+1
View File
@@ -187,6 +187,7 @@ mod moving_pistons;
mod music;
mod names;
mod narrator;
mod native_biome_colors;
mod native_block_entity_draw;
mod native_components;
mod native_criteria;
@@ -0,0 +1,39 @@
//! Biome colours a client mod changes (`native_biome_colors`), for seasons:
//! the grass, foliage, dry foliage and water tints the mesher blends, as a
//! resource pack's colormaps and biome effects set them, changed while the
//! game runs.
use super::Game;
use horizon_client::streaming::WorkerContext;
use std::sync::Arc;
// Native mods call it; nothing in Horizon does.
#[allow(dead_code)]
impl Game<'_> {
/// Asks `native_biome_colors` again for every biome and, when the colours
/// differ from those in use, rebuilds every loaded section and the far
/// terrain with them (`LevelRenderer.allChanged`, as changing Biome Blend
/// does). Call it when your colours change (a new season, a new day of a
/// slow fade) and once when the player joins, not every frame: a rebuild
/// takes the workers a few seconds. API @API:biome_colors@.
pub(super) fn native_refresh_biome_colors(&mut self) {
let base = self.res.biomes().clone();
let biomes = base
.with_colors(|biome, colors| self.native_biome_colors(biome, colors))
.map_or(base, Arc::new);
if Arc::ptr_eq(&biomes, &self.pack.context.biomes) {
return;
}
let old = &self.pack.context;
let context = Arc::new(WorkerContext {
assets: old.assets.clone(),
render: old.render.clone(),
tables: old.tables.clone(),
biomes,
light: old.light.clone(),
});
self.pack.context = context.clone();
self.clear_lod();
self.streaming.reload_resources(&mut self.world, context);
}
}
@@ -282,6 +282,7 @@ use super::{
};
use horizon_client::{
atlas_sources::qualified,
biome::BiomeColors,
block_entities::Pos,
chat::ChatSource,
containers::{BlockEntity, MOD_MENUS, ModBlockEntity, ModContainer, ModFluidTanks},
@@ -1362,6 +1363,17 @@ impl Game<'_> {
/// one that pack is for. Asked once a frame while a pack is on; a value
/// that isn't finite reads as 0 (`native_shader_params_now`). API
/// @API:native_shader_params@.
/// Change the colours of `biome` (`minecraft:plains`) on this client: the
/// grass, foliage (leaves, vines), dry foliage (leaf litter) and water
/// tints, 0xRRGGBB, as its biome file and the colormaps set them. Asked
/// for every biome when a mod calls `native_refresh_biome_colors`, which
/// rebuilds the terrain with them; a season mod answers from its season
/// and calls that when the season turns. The dark forest's modifier
/// still applies to the grass set here; the swamp's noise colours
/// replace it. API @API:biome_colors@.
pub(super) fn native_biome_colors(&self, biome: &str, colors: &mut BiomeColors) {
let _ = (biome, colors);
}
pub(super) fn native_shader_params(&self, partial: f32, params: &mut [[f32; 4]; 8]) {
let _ = (partial, params);
}
+16
View File
@@ -1133,6 +1133,22 @@ explosion's centre (camera-relative, from `frame.camera_position`) and its age i
ray marched in a post pass against `scene_depth`. A value that isn't finite reads as 0, and all are 0 when no mod sets
them. The mod's server half can send what happened to each client half with mod messages (API 40).
### Biome colours
`crates/horizon-client/src/game/native_biome_colors.rs`: the grass, leaf and water colours a client mod changes while the
game runs, for seasons (a golden autumn, a pale winter) or a world that greens as it heals.
- `native_biome_colors(biome, colors)` (client) changes a biome's `BiomeColors`: `grass`, `foliage` (leaves, vines),
`dry_foliage` (leaf litter) and `water`, 0xRRGGBB, starting from the biome file and the colormaps. The dark forest's
modifier still darkens the grass set here; the swamp's noise colours replace it.
- It is asked for every biome when a mod calls `native_refresh_biome_colors()`. When the colours differ from those in use,
every loaded section and the far terrain are rebuilt with them, as changing Biome Blend does. Call it when your colours
change and once when the player joins, not every tick: a rebuild takes the workers a few seconds. A slow fade is a
call a day, not a frame.
- Block-breaking and falling-leaf particles keep the biome's own colours for now.
- A season's mood in a shader pack (warmer grading, a lower sun, more haze) comes from the same mod through
`native_shader_params` (`frame.mods`); its server half can send the season to each client with mod messages.
### Sounds a client mod plays
`native_play_local_sound(sound, at, volume, pitch, delay)` (client) plays sound event `sound` at `at` for this player only,
@@ -0,0 +1,14 @@
{
"hooks": [
{
"name": "biome_colors",
"target": "horizon_client::game::native_hooks::Game<'_>::native_biome_colors",
"side": "client"
}
],
"rust_summary": "biome colours for client mods (`native_biome_colors`, `native_refresh_biome_colors`).",
"doc_summary": "biome colours for client mods, for seasons: `native_biome_colors(biome, colors)` changes a biome's grass, foliage, dry\nfoliage and water tints, asked for every biome when a mod calls `native_refresh_biome_colors()`, which rebuilds the\nloaded terrain with them. Bundles that use it need `\"hooks\": @API:biome_colors@`.",
"comments": [
"crates/horizon-client/src/game/native_biome_colors.rs"
]
}