From 3d13fd331e47b3ce1c216f9c4aa6c73b78bb7258 Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Fri, 25 Sep 2026 09:20:58 +0100
Subject: [PATCH] MONKEY (fog): LightFogBand.dbc reader +
LightCatalog::blend_chain
benilla-formats/src/light/fog_band.rs reads our LightFogBand.dbc (the
LightFloatBand record, 13 rows per LightParams: height fog, curve blend,
sun-fog rgb/strength/angle, end-fog rgb/distance). Absent file = empty
catalog, no error. blend_chain exposes the area-light blend order so a
side table folds with the same law as the stock bands.
Co-Authored-By: Claude Opus 5.5
---
crates/benilla-formats/src/lib.rs | 2 +
crates/benilla-formats/src/light.rs | 63 ++++++
crates/benilla-formats/src/light/fog_band.rs | 201 +++++++++++++++++++
crates/benilla-formats/src/light/tables.rs | 10 +
4 files changed, 276 insertions(+)
create mode 100644 crates/benilla-formats/src/light/fog_band.rs
diff --git a/crates/benilla-formats/src/lib.rs b/crates/benilla-formats/src/lib.rs
index ad87a31e..174796de 100644
--- a/crates/benilla-formats/src/lib.rs
+++ b/crates/benilla-formats/src/lib.rs
@@ -128,6 +128,8 @@ pub use ground_effects::{
};
mod light;
pub use light::{Atmosphere, LightCatalog, Submersion, ZERO_KEY_COLOR, ZERO_KEY_SCALAR};
+// MONKEY (fog): `LightFogBand.dbc`.
+pub use light::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM};
mod loading_screen;
pub use loading_screen::{load_loading_screens, LoadingScreenCatalog};
mod liquid;
diff --git a/crates/benilla-formats/src/light.rs b/crates/benilla-formats/src/light.rs
index 3edc847e..cb19d6de 100644
--- a/crates/benilla-formats/src/light.rs
+++ b/crates/benilla-formats/src/light.rs
@@ -11,9 +11,12 @@ use crate::dbc::{f32_at, parse, str_at, u32_at};
use crate::Chain;
mod atmosphere;
+// MONKEY (fog): `LightFogBand.dbc`, the modern fog fields.
+mod fog_band;
mod tables;
pub use atmosphere::Atmosphere;
+pub use fog_band::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM};
use atmosphere::{
FB_CLOUD_DENSITY, FB_FOG_END, FB_FOG_START_MULT, IB_AMBIENT, IB_CLOUD_GBASE, IB_CLOUD_SLOPE,
IB_CLOUD_SUN, IB_DIFFUSE, IB_FOG_COLOR, IB_OCEAN_DEEP, IB_OCEAN_SHALLOW, IB_RIVER_DEEP,
@@ -509,6 +512,66 @@ impl LightCatalog {
acc
}
+ /// MONKEY (fog): the `LightParams` ids [`Self::sample_blended`] folds, in its order, each with its
+ /// lerp weight (the base first at 1.0). A side table keyed by `LightParams` (`LightFogBand.dbc`)
+ /// blends through this with the same law. Magma and slime give their fixed row alone.
+ pub fn blend_chain(
+ &self,
+ map: u32,
+ pos: [f32; 3],
+ stormy: bool,
+ submersion: Submersion,
+ ghost: bool,
+ ) -> Vec<(u32, f32)> {
+ if let Some(p) = submersion.fixed_param() {
+ if self.has_param(p) {
+ return vec![(p, 1.0)];
+ }
+ }
+ let slot = weather_slot(ghost, stormy, submersion.is_water());
+ let param_of = |l: &Light| match l.params[slot] {
+ 0 => l.params[SLOT_CLEAR],
+ p => p,
+ };
+ let mut chain = Vec::new();
+ let map_has_no_light = !self.lights.iter().any(|l| l.map == map);
+ if let Some(base) = self
+ .lights
+ .iter()
+ .find(|l| l.map == map && l.global)
+ .or_else(|| {
+ map_has_no_light
+ .then(|| self.lights.iter().find(|l| l.id == FALLBACK_LIGHT_ID))
+ .flatten()
+ })
+ {
+ let p = param_of(base);
+ if p >= 1 {
+ chain.push((p, 1.0));
+ }
+ }
+ let mut locals: Vec<(f32, &Light)> = self
+ .lights
+ .iter()
+ .filter(|l| l.map == map && !l.global)
+ .filter_map(|l| {
+ let d = (0..3)
+ .map(|i| (l.pos[i] - pos[i]).powi(2))
+ .sum::()
+ .sqrt();
+ (d <= l.falloff_end).then_some((d, l))
+ })
+ .collect();
+ locals.sort_by(|a, b| b.0.total_cmp(&a.0));
+ for (dist, l) in locals {
+ let p = param_of(l);
+ if p >= 1 {
+ chain.push((p, blend_alpha(dist, l.falloff_start, l.falloff_end)));
+ }
+ }
+ chain
+ }
+
/// Debug: the spheres near `pos`, nearest first (the blend applies them farthest first), then
/// the blended result against the `pick_light` sample and the water swatch endpoints.
pub fn debug_blend(&self, map: u32, pos: [f32; 3], time: u32) {
diff --git a/crates/benilla-formats/src/light/fog_band.rs b/crates/benilla-formats/src/light/fog_band.rs
new file mode 100644
index 00000000..56aea8f5
--- /dev/null
+++ b/crates/benilla-formats/src/light/fog_band.rs
@@ -0,0 +1,201 @@
+//! MONKEY (fog): `LightFogBand.dbc`, the modern fog fields 1.12 has no column for.
+//!
+//! The file is ours (DESIGN-Benilla-Graphics §2a). It mirrors `LightFloatBand.dbc` exactly: the
+//! same 34-field, 136-byte band record (`ID, num, time[16], value[16]`, times in half-minutes on
+//! the 2880 day), interpolated the same way across midnight. `LightParams` id `P` keys
+//! [`FOG_BANDS_PER_PARAM`] rows, band `b` at id `(P − 1)·13 + b + 1`, the `LightFloatBand`
+//! indexing with its own stride, so the stock `paramID·6 + k` tables stay untouched.
+//!
+//! | band | field | unit |
+//! |---|---|---|
+//! | 0 | height-fog density | extra extinction below the plane, × the distance fog's (0 = off) |
+//! | 1 | height-fog height | WoW world Z, yards |
+//! | 2 | height-fog falloff | 1/yd: how fast the extra density fades above the plane |
+//! | 3 | curve blend | 0 = exponential (modern), 1 = the 1.12 linear ramp |
+//! | 4-6 | sun-fog colour r, g, b | gamma 0..1 |
+//! | 7 | sun-fog strength | 0..1 |
+//! | 8 | sun-fog angle | cosine threshold of the lobe around the sun |
+//! | 9-11 | end-fog colour r, g, b | gamma 0..1 |
+//! | 12 | end-fog distance | yards (0 = the scene fog end) |
+//!
+//! A missing file, or a `LightParams` id without rows, is not an error: the caller falls back to
+//! the colours it derives from the 1.12 bands.
+
+use std::collections::HashMap;
+
+use benilla_dbc::FieldType;
+
+use super::tables::{band_schema, load_float_bands, sample_float, Band};
+use crate::Chain;
+
+const FOG_BAND: &str = "DBFilesClient\\LightFogBand.dbc";
+
+/// Rows per `LightParams` id.
+pub const FOG_BANDS_PER_PARAM: u32 = 13;
+
+/// One `LightParams` id's fog fields at one time of day (see the module table).
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct FogBand {
+ pub height_density: f32,
+ /// WoW world Z, yards.
+ pub height: f32,
+ pub height_falloff: f32,
+ pub curve_blend: f32,
+ pub sun_rgb: [f32; 3],
+ pub sun_strength: f32,
+ pub sun_angle: f32,
+ pub end_rgb: [f32; 3],
+ /// Yards; 0 = the scene fog end.
+ pub end_distance: f32,
+}
+
+impl FogBand {
+ /// Componentwise `self + (o − self)·t`, the area and storm blend.
+ pub fn lerp(&self, o: &FogBand, t: f32) -> FogBand {
+ let l = |a: f32, b: f32| a + (b - a) * t;
+ let l3 = |a: [f32; 3], b: [f32; 3]| [l(a[0], b[0]), l(a[1], b[1]), l(a[2], b[2])];
+ FogBand {
+ height_density: l(self.height_density, o.height_density),
+ height: l(self.height, o.height),
+ height_falloff: l(self.height_falloff, o.height_falloff),
+ curve_blend: l(self.curve_blend, o.curve_blend),
+ sun_rgb: l3(self.sun_rgb, o.sun_rgb),
+ sun_strength: l(self.sun_strength, o.sun_strength),
+ sun_angle: l(self.sun_angle, o.sun_angle),
+ end_rgb: l3(self.end_rgb, o.end_rgb),
+ end_distance: l(self.end_distance, o.end_distance),
+ }
+ }
+}
+
+/// The parsed `LightFogBand.dbc`; empty when the file is absent.
+#[derive(Default)]
+pub struct FogBandCatalog {
+ bands: HashMap>,
+}
+
+impl FogBandCatalog {
+ /// Read `LightFogBand.dbc` off the chain. Absent or unreadable → an empty catalog (every
+ /// lookup `None`), which is the 1.12 data set.
+ pub fn load(chain: &mut Chain) -> Self {
+ let bands = load_float_bands(
+ chain,
+ FOG_BAND,
+ band_schema("LightFogBand", FieldType::Float32),
+ )
+ .unwrap_or_default();
+ FogBandCatalog { bands }
+ }
+
+ /// Whether the file carried any row.
+ pub fn is_empty(&self) -> bool {
+ self.bands.is_empty()
+ }
+
+ /// `LightParams` id `p` at `time` (half-minutes), or `None` when `p` has no rows. A single
+ /// missing band of a present id reads 0.
+ pub fn sample(&self, p: u32, time: u32) -> Option {
+ if p < 1 {
+ return None;
+ }
+ let base = (p - 1) * FOG_BANDS_PER_PARAM + 1;
+ let rows: Vec