1#[expect(
2 clippy::struct_field_names,
3 reason = "field names match vanilla weather state naming"
4)]
5#[derive(Debug, Default)]
6pub struct Weather {
7 pub rain_level: f32,
8 pub previous_rain_level: f32,
9 pub thunder_level: f32,
10 pub previous_thunder_level: f32,
11}
12
13use super::{
14 ADVANCE_WEATHER, BiomeRef, BlockPos, CGameEvent, ChunkGenerator, ChunkPos, GameEventType,
15 HeightmapType, LazyLock, LegacyRandom, LightLayer, PerlinSimplexNoise, REGISTRY, RandomSource,
16 RegistryEntry, RegistryExt, TemperatureModifier, World, environment, fuzzed_biome_at_block,
17 obfuscate_biome_seed, vanilla_dimension_types,
18};
19
20static BIOME_TEMPERATURE_NOISE: LazyLock<PerlinSimplexNoise> = LazyLock::new(|| {
21 let mut random = RandomSource::Legacy(LegacyRandom::from_seed(1234));
22 PerlinSimplexNoise::new(&mut random, &[0])
23});
24
25static FROZEN_BIOME_TEMPERATURE_NOISE: LazyLock<PerlinSimplexNoise> = LazyLock::new(|| {
26 let mut random = RandomSource::Legacy(LegacyRandom::from_seed(3456));
27 PerlinSimplexNoise::new(&mut random, &[-2, -1, 0])
28});
29
30static BIOME_INFO_NOISE: LazyLock<PerlinSimplexNoise> = LazyLock::new(|| {
31 let mut random = RandomSource::Legacy(LegacyRandom::from_seed(2345));
32 PerlinSimplexNoise::new(&mut random, &[0])
33});
34
35impl World {
36 #[expect(
37 clippy::too_many_lines,
38 reason = "splitting would hurt readability of the weather state machine"
39 )]
40 pub(super) fn tick_weather(&self) {
41 if !self.can_have_weather() {
42 return;
43 }
44
45 let mut weather = self.weather.lock();
46 let raining_before = self.is_raining_with_guard(&weather);
47
48 {
50 let mut level_data = self.level_data.write();
51
52 if self.get_game_rule_with_guard(&ADVANCE_WEATHER, &level_data) {
53 let clear_weather_time = level_data.clear_weather_time();
54 if clear_weather_time > 0 {
55 level_data.set_clear_weather_time(clear_weather_time - 1);
56 if level_data.is_thundering() {
57 level_data.set_thunder_time(0);
58 level_data.set_thundering(false);
59 } else {
60 level_data.set_thunder_time(1);
61 }
62 if level_data.is_raining() {
63 level_data.set_rain_time(0);
64 level_data.set_raining(false);
65 } else {
66 level_data.set_rain_time(1);
67 }
68 } else {
69 let thundering_time = level_data.thunder_time();
70 if thundering_time > 0 {
71 level_data.set_thunder_time(thundering_time - 1);
72 if level_data.thunder_time() == 0 {
73 let thundering = level_data.is_thundering();
74 level_data.set_thundering(!thundering);
75 }
76 } else if level_data.is_thundering() {
77 level_data.set_thunder_time(rand::random_range(3_600..=15_600));
78 } else {
79 level_data.set_thunder_time(rand::random_range(12_000..=180_000));
80 }
81
82 let rain_time = level_data.rain_time();
83 if rain_time > 0 {
84 level_data.set_rain_time(rain_time - 1);
85 if level_data.rain_time() == 0 {
86 let raining = level_data.is_raining();
87 level_data.set_raining(!raining);
88 }
89 } else if level_data.is_raining() {
90 level_data.set_rain_time(rand::random_range(12_000..=24_000));
91 } else {
92 level_data.set_rain_time(rand::random_range(12_000..=180_000));
93 }
94 }
95 }
96 }
97
98 let is_thundering = self.level_data.read().is_thundering();
100 let is_raining = self.level_data.read().is_raining();
101
102 weather.previous_thunder_level = weather.thunder_level;
103 if is_thundering {
104 weather.thunder_level += 0.01;
105 } else {
106 weather.thunder_level -= 0.01;
107 }
108 weather.thunder_level = weather.thunder_level.clamp(0.0, 1.0);
109
110 weather.previous_rain_level = weather.rain_level;
111 if is_raining {
112 weather.rain_level += 0.01;
113 } else {
114 weather.rain_level -= 0.01;
115 }
116 weather.rain_level = weather.rain_level.clamp(0.0, 1.0);
117
118 let raining_now = self.is_raining_with_guard(&weather);
120 if raining_before == raining_now {
121 #[expect(
122 clippy::float_cmp,
123 reason = "comparing against the exact previously-assigned value to detect any change"
124 )]
125 if weather.previous_rain_level != weather.rain_level {
126 self.broadcast_to_all(CGameEvent {
127 event: GameEventType::RainLevelChange,
128 data: weather.rain_level,
129 });
130 }
131
132 #[expect(
133 clippy::float_cmp,
134 reason = "comparing against the exact previously-assigned value to detect any change"
135 )]
136 if weather.previous_thunder_level != weather.thunder_level {
137 self.broadcast_to_all(CGameEvent {
138 event: GameEventType::ThunderLevelChange,
139 data: weather.thunder_level,
140 });
141 }
142 } else {
143 if raining_before {
144 self.broadcast_to_all(CGameEvent {
145 event: GameEventType::StopRaining,
146 data: 0.0,
147 });
148 } else {
149 self.broadcast_to_all(CGameEvent {
150 event: GameEventType::StartRaining,
151 data: 0.0,
152 });
153 }
154
155 self.broadcast_to_all(CGameEvent {
156 event: GameEventType::RainLevelChange,
157 data: weather.rain_level,
158 });
159
160 self.broadcast_to_all(CGameEvent {
161 event: GameEventType::ThunderLevelChange,
162 data: weather.thunder_level,
163 });
164 }
165 }
166
167 pub(crate) fn set_weather_parameters(
172 &self,
173 clear_time: i32,
174 rain_time: i32,
175 raining: bool,
176 thundering: bool,
177 ) {
178 let mut level_data = self.level_data.write();
179 level_data.set_clear_weather_time(clear_time);
180 level_data.set_rain_time(rain_time);
181 level_data.set_thunder_time(rain_time);
182 level_data.set_raining(raining);
183 level_data.set_thundering(thundering);
184 }
185
186 pub fn is_raining(&self) -> bool {
192 let guard = self.weather.lock();
193 self.is_raining_with_guard(&guard)
194 }
195
196 pub fn is_raining_at(&self, pos: BlockPos) -> bool {
201 if !self.is_raining() || !self.can_see_sky_for_precipitation(pos) {
202 return false;
203 }
204
205 self.biome_at(pos).is_some_and(|biome| {
206 biome.has_precipitation && self.biome_temperature(biome, pos) >= 0.15
207 })
208 }
209
210 pub fn is_raining_with_guard(&self, guard: &Weather) -> bool {
212 guard.rain_level > 0.2 && self.can_have_weather()
213 }
214
215 pub fn is_thundering(&self) -> bool {
221 let guard = self.weather.lock();
222 self.is_thundering_with_guard(&guard)
223 }
224
225 pub fn is_thundering_with_guard(&self, guard: &Weather) -> bool {
227 guard.rain_level * guard.thunder_level > 0.9 && self.can_have_weather()
228 }
229
230 pub fn sky_light_level(&self) -> f32 {
232 let (rain_level, thunder_level) = if self.can_have_weather() {
233 let weather = self.weather.lock();
234 (weather.rain_level, weather.thunder_level)
235 } else {
236 (0.0, 0.0)
237 };
238
239 let level_data = self.level_data.read();
240 environment::sky_light_level(
241 self.dimension_type,
242 level_data.world_clocks(),
243 rain_level,
244 thunder_level,
245 self.can_have_weather(),
246 )
247 }
248
249 pub fn sky_darkening(&self) -> u8 {
251 environment::sky_darkening(self.sky_light_level())
252 }
253
254 pub fn sun_angle_degrees(&self) -> f32 {
256 let level_data = self.level_data.read();
257 environment::sun_angle_degrees(self.dimension_type, level_data.world_clocks())
258 }
259
260 pub fn effective_sky_brightness(&self, pos: BlockPos) -> u8 {
265 if !self.dimension_type.has_skylight {
266 return 0;
267 }
268 self.light_value_at(LightLayer::Sky, pos)
269 .saturating_sub(self.sky_darkening())
270 }
271
272 pub fn is_bright_outside(&self) -> bool {
274 self.dimension_type.fixed_time.is_none() && self.sky_darkening() < 4
275 }
276
277 pub fn is_dark_outside(&self) -> bool {
279 self.dimension_type.fixed_time.is_none() && !self.is_bright_outside()
280 }
281
282 pub fn can_have_weather(&self) -> bool {
284 self.dimension_type.has_skylight
285 && !self.dimension_type.has_ceiling
286 && self.dimension_type.key != vanilla_dimension_types::THE_END.key
287 }
288
289 pub fn can_see_sky(&self, pos: BlockPos) -> bool {
294 if !self.dimension_type.has_skylight {
295 return false;
296 }
297 self.height_at(HeightmapType::MotionBlocking, pos.x(), pos.z())
298 .is_some_and(|height| height <= pos.y())
299 }
300
301 pub(super) fn can_see_sky_for_precipitation(&self, pos: BlockPos) -> bool {
302 self.can_see_sky(pos)
303 }
304
305 pub(crate) fn biome_at(&self, pos: BlockPos) -> Option<BiomeRef> {
306 let biome_zoom_seed = obfuscate_biome_seed(self.seed());
307 let mut missing_chunk = false;
308 let biome_id = fuzzed_biome_at_block(biome_zoom_seed, pos, |quart| {
309 self.noise_biome_id(quart.x, quart.y, quart.z)
310 .unwrap_or_else(|| {
311 missing_chunk = true;
312 0
313 })
314 });
315
316 if missing_chunk {
317 return None;
318 }
319
320 REGISTRY.biomes.by_id(usize::from(biome_id))
321 }
322
323 pub(super) fn noise_biome_id(&self, quart_x: i32, quart_y: i32, quart_z: i32) -> Option<u16> {
324 let chunk_pos = ChunkPos::new(quart_x >> 2, quart_z >> 2);
325 let local_quart_x = (quart_x & 3) as usize;
326 let local_quart_z = (quart_z & 3) as usize;
327
328 if let Some(Some(biome_id)) = self.chunk_map.with_full_chunk(chunk_pos, |chunk| {
329 let sections = chunk.sections();
330 let (section_index, local_quart_y) =
331 Self::biome_quart_y_indices(chunk.min_y(), sections.sections.len(), quart_y)?;
332 let section = sections.sections.get(section_index)?;
333 Some(
334 section
335 .read()
336 .biomes
337 .get(local_quart_x, local_quart_y, local_quart_z),
338 )
339 }) {
340 return Some(biome_id);
341 }
342
343 let biome = self
344 .chunk_map
345 .world_gen_context
346 .generator
347 .noise_biome(quart_x, quart_y, quart_z);
348 u16::try_from(biome.try_id()?).ok()
349 }
350
351 pub(super) fn biome_quart_y_indices(
352 min_y: i32,
353 section_count: usize,
354 quart_y: i32,
355 ) -> Option<(usize, usize)> {
356 let total_quart_y = section_count.checked_mul(4)?;
357 if total_quart_y == 0 {
358 return None;
359 }
360
361 let relative_quart_y = i64::from(quart_y) - i64::from(min_y >> 2);
362 let max_relative_quart_y = total_quart_y - 1;
363 let clamped_relative_quart_y = if relative_quart_y <= 0 {
364 0
365 } else {
366 usize::try_from(relative_quart_y).map_or(max_relative_quart_y, |relative| {
367 relative.min(max_relative_quart_y)
368 })
369 };
370
371 Some((clamped_relative_quart_y / 4, clamped_relative_quart_y & 3))
372 }
373
374 pub(super) fn biome_temperature(&self, biome: BiomeRef, pos: BlockPos) -> f32 {
375 let modified_temp = match biome.temperature_modifier {
376 TemperatureModifier::None => biome.temperature,
377 TemperatureModifier::Frozen => {
378 let large = FROZEN_BIOME_TEMPERATURE_NOISE
379 .get_value(f64::from(pos.x()) * 0.05, f64::from(pos.z()) * 0.05)
380 * 7.0;
381 let edge =
382 BIOME_INFO_NOISE.get_value(f64::from(pos.x()) * 0.2, f64::from(pos.z()) * 0.2);
383 if large + edge < 0.3 {
384 let small = BIOME_INFO_NOISE
385 .get_value(f64::from(pos.x()) * 0.09, f64::from(pos.z()) * 0.09);
386 if small < 0.8 {
387 return 0.2;
388 }
389 }
390 biome.temperature
391 }
392 };
393
394 let snow_level = self.sea_level + 17;
395 if pos.y() <= snow_level {
396 return modified_temp;
397 }
398
399 let noise = BIOME_TEMPERATURE_NOISE
400 .get_value(f64::from(pos.x()) / 8.0, f64::from(pos.z()) / 8.0)
401 as f32
402 * 8.0;
403 modified_temp - (noise + pos.y() as f32 - snow_level as f32) * 0.05 / 40.0
404 }
405}