veloren_server/weather/
tick.rs

1use common::{
2    comp,
3    event::EventBus,
4    outcome::Outcome,
5    resources::{DeltaTime, ProgramTime, TimeOfDay},
6    slowjob::{SlowJob, SlowJobPool},
7    weather::{SharedWeatherGrid, Weather, WeatherGrid},
8};
9use common_ecs::{Origin, Phase, System};
10use common_net::msg::ServerGeneral;
11use rand::{Rng, seq::SliceRandom, thread_rng};
12use specs::{Entities, Join, Read, ReadExpect, ReadStorage, Write, WriteExpect};
13use std::{mem, sync::Arc};
14use vek::Vec2;
15use world::World;
16
17use crate::{Tick, client::Client};
18
19use super::{
20    WEATHER_DT,
21    sim::{LightningCells, WeatherSim},
22};
23
24enum WeatherJobState {
25    #[expect(dead_code)]
26    Working(SlowJob),
27    Idle(WeatherSim),
28    None,
29}
30
31pub struct WeatherJob {
32    last_update: ProgramTime,
33    weather_tx: crossbeam_channel::Sender<(WeatherGrid, LightningCells, WeatherSim)>,
34    weather_rx: crossbeam_channel::Receiver<(WeatherGrid, LightningCells, WeatherSim)>,
35    state: WeatherJobState,
36    qeued_zones: Vec<(Weather, Vec2<f32>, f32, f32)>,
37}
38
39impl WeatherJob {
40    pub fn queue_zone(&mut self, weather: Weather, pos: Vec2<f32>, radius: f32, time: f32) {
41        self.qeued_zones.push((weather, pos, radius, time))
42    }
43}
44
45#[derive(Default)]
46pub struct Sys;
47
48impl<'a> System<'a> for Sys {
49    type SystemData = (
50        Entities<'a>,
51        Read<'a, TimeOfDay>,
52        Read<'a, ProgramTime>,
53        Read<'a, Tick>,
54        Read<'a, DeltaTime>,
55        Write<'a, LightningCells>,
56        Write<'a, Option<WeatherJob>>,
57        WriteExpect<'a, WeatherGrid>,
58        WriteExpect<'a, SlowJobPool>,
59        Read<'a, EventBus<Outcome>>,
60        ReadExpect<'a, Arc<World>>,
61        ReadStorage<'a, Client>,
62        ReadStorage<'a, comp::Pos>,
63    );
64
65    const NAME: &'static str = "weather::tick";
66    const ORIGIN: Origin = Origin::Server;
67    const PHASE: Phase = Phase::Create;
68
69    fn run(
70        _job: &mut common_ecs::Job<Self>,
71        (
72            entities,
73            game_time,
74            program_time,
75            tick,
76            delta_time,
77            mut lightning_cells,
78            mut weather_job,
79            mut grid,
80            slow_job_pool,
81            outcomes,
82            world,
83            clients,
84            positions,
85        ): Self::SystemData,
86    ) {
87        let to_update = match &mut *weather_job {
88            Some(weather_job) => (program_time.0 - weather_job.last_update.0 >= WEATHER_DT as f64)
89                .then_some(weather_job),
90            None => {
91                let (weather_tx, weather_rx) = crossbeam_channel::bounded(1);
92
93                let weather_size = world.sim().get_size() / common::weather::CHUNKS_PER_CELL;
94                let mut sim = WeatherSim::new(weather_size, &world);
95                *grid = WeatherGrid::new(sim.size());
96                *lightning_cells = sim.tick(*game_time, &mut grid);
97
98                *weather_job = Some(WeatherJob {
99                    last_update: *program_time,
100                    weather_tx,
101                    weather_rx,
102                    state: WeatherJobState::Idle(sim),
103                    qeued_zones: Vec::new(),
104                });
105
106                None
107            },
108        };
109
110        if let Some(weather_job) = to_update {
111            if matches!(weather_job.state, WeatherJobState::Working(_))
112                && let Ok((new_grid, new_lightning_cells, sim)) = weather_job.weather_rx.try_recv()
113            {
114                *grid = new_grid;
115                *lightning_cells = new_lightning_cells;
116                let mut lazy_msg = None;
117                for client in clients.join() {
118                    if lazy_msg.is_none() {
119                        lazy_msg = Some(client.prepare(ServerGeneral::WeatherUpdate(
120                            SharedWeatherGrid::from(&*grid),
121                        )));
122                    }
123                    lazy_msg.as_ref().map(|msg| client.send_prepared(msg));
124                }
125                weather_job.state = WeatherJobState::Idle(sim);
126            }
127
128            if matches!(weather_job.state, WeatherJobState::Idle(_)) {
129                weather_job.last_update = *program_time;
130                let old_state = mem::replace(&mut weather_job.state, WeatherJobState::None);
131
132                let WeatherJobState::Idle(mut sim) = old_state else {
133                    unreachable!()
134                };
135
136                let weather_tx = weather_job.weather_tx.clone();
137                let game_time = *game_time;
138                for (weather, pos, radius, time) in weather_job.qeued_zones.drain(..) {
139                    sim.add_zone(weather, pos, radius, time)
140                }
141                let job = slow_job_pool.spawn("WEATHER", move || {
142                    let mut grid = WeatherGrid::new(sim.size());
143                    let lightning_cells = sim.tick(game_time, &mut grid);
144                    let _ = weather_tx.send((grid, lightning_cells, sim));
145                });
146
147                weather_job.state = WeatherJobState::Working(job);
148            }
149        }
150
151        // Chance to emit lightning every frame from one or more of the cells that
152        // currently has the correct weather conditions.
153        let mut outcome_emitter = outcomes.emitter();
154        let mut rng = thread_rng();
155        let num_cells = lightning_cells.cells.len() as f64 * 0.0015 * delta_time.0 as f64;
156        let num_cells = num_cells.floor() as u32 + rng.gen_bool(num_cells.fract()) as u32;
157
158        for _ in 0..num_cells {
159            let cell_pos = lightning_cells.cells.choose(&mut rng).expect(
160                "This is non-empty, since we multiply with its len for the chance to do a \
161                 lightning strike.",
162            );
163            let wpos = cell_pos
164                .map(|e| (e as f32 + rng.gen_range(0.0..1.0)) * common::weather::CELL_SIZE as f32);
165            outcome_emitter.emit(Outcome::Lightning {
166                pos: wpos.with_z(world.sim().get_alt_approx(wpos.as_()).unwrap_or(0.0)),
167            });
168        }
169
170        for (entity, client, pos) in (&entities, &clients, &positions).join() {
171            if entity.id() as u64 % 30 == tick.0 % 30 {
172                let weather = grid.get_interpolated(pos.0.xy());
173                client.send_fallible(ServerGeneral::LocalWindUpdate(weather.wind));
174            }
175        }
176    }
177}