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