veloren_server/rtsim/
mod.rs

1pub mod event;
2pub mod rule;
3pub mod tick;
4
5use atomicwrites::{AtomicFile, OverwriteBehavior};
6use common::{
7    grid::Grid,
8    mounting::VolumePos,
9    rtsim::{Actor, ChunkResource, NpcId, RtSimEntity, WorldSettings},
10    terrain::{CoordinateConversions, SpriteKind},
11};
12use common_ecs::{System, dispatch};
13use common_state::BlockDiff;
14use crossbeam_channel::{Receiver, Sender, unbounded};
15use enum_map::EnumMap;
16use rtsim::{
17    RtState,
18    data::{Data, ReadError, npc::SimulationMode},
19    event::{OnDeath, OnHealthChange, OnHelped, OnMountVolume, OnSetup, OnTheft},
20};
21use specs::DispatcherBuilder;
22use std::{
23    fs::{self, File},
24    io,
25    path::PathBuf,
26    thread::{self, JoinHandle},
27    time::Instant,
28};
29use tracing::{debug, error, info, trace, warn};
30use vek::*;
31use world::{IndexRef, World};
32
33pub struct RtSim {
34    file_path: PathBuf,
35    last_saved: Option<Instant>,
36    state: RtState,
37    save_thread: Option<(Sender<Data>, JoinHandle<()>)>,
38}
39
40impl RtSim {
41    pub fn new(
42        settings: &WorldSettings,
43        index: IndexRef,
44        world: &World,
45        data_dir: PathBuf,
46    ) -> Result<Self, ron::Error> {
47        let file_path = Self::get_file_path(data_dir);
48
49        info!("Looking for rtsim data at {}...", file_path.display());
50        let data = 'load: {
51            if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") {
52                match File::open(&file_path) {
53                    Ok(file) => {
54                        info!("Rtsim data found. Attempting to load...");
55
56                        let ignore_version = std::env::var("RTSIM_IGNORE_VERSION").is_ok();
57
58                        match Data::from_reader(io::BufReader::new(file)) {
59                            Err(ReadError::VersionMismatch(_)) if !ignore_version => {
60                                warn!(
61                                    "Rtsim data version mismatch (implying a breaking change), \
62                                     rtsim data will be purged"
63                                );
64                            },
65                            Ok(data) | Err(ReadError::VersionMismatch(data)) => {
66                                info!("Rtsim data loaded.");
67                                if data.should_purge {
68                                    warn!(
69                                        "The should_purge flag was set on the rtsim data, \
70                                         generating afresh"
71                                    );
72                                } else {
73                                    break 'load *data;
74                                }
75                            },
76                            Err(ReadError::Load(err)) => {
77                                error!("Rtsim data failed to load: {}", err);
78                                info!("Old rtsim data will now be moved to a backup file");
79                                let mut i = 0;
80                                loop {
81                                    let mut backup_path = file_path.clone();
82                                    backup_path.set_extension(if i == 0 {
83                                        "ron_backup".to_string()
84                                    } else {
85                                        format!("ron_backup_{}", i)
86                                    });
87                                    if !backup_path.exists() {
88                                        fs::rename(&file_path, &backup_path)?;
89                                        warn!(
90                                            "Failed rtsim data was moved to {}",
91                                            backup_path.display()
92                                        );
93                                        info!("A fresh rtsim data will now be generated.");
94                                        break;
95                                    } else {
96                                        info!(
97                                            "Backup file {} already exists, trying another name...",
98                                            backup_path.display()
99                                        );
100                                    }
101                                    i += 1;
102                                }
103                            },
104                        }
105                    },
106                    Err(e) if e.kind() == io::ErrorKind::NotFound => {
107                        info!("No rtsim data found. Generating from world...")
108                    },
109                    Err(e) => return Err(e.into()),
110                }
111            } else {
112                warn!(
113                    "'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be \
114                     overwritten)."
115                );
116            }
117
118            let data = Data::generate(settings, world, index);
119            info!("Rtsim data generated.");
120            data
121        };
122
123        let mut this = Self {
124            last_saved: None,
125            state: RtState::new(data).with_resource(ChunkStates(Grid::populate_from(
126                world.sim().get_size().as_(),
127                |_| None,
128            ))),
129            file_path,
130            save_thread: None,
131        };
132
133        rule::start_rules(&mut this.state);
134
135        this.state.emit(OnSetup, &mut (), world, index);
136
137        Ok(this)
138    }
139
140    fn get_file_path(mut data_dir: PathBuf) -> PathBuf {
141        let mut path = std::env::var("VELOREN_RTSIM")
142            .map(PathBuf::from)
143            .unwrap_or_else(|_| {
144                data_dir.push("rtsim");
145                data_dir
146            });
147        path.push("data.dat");
148        path
149    }
150
151    pub fn hook_character_mount_volume(
152        &mut self,
153        world: &World,
154        index: IndexRef,
155        pos: VolumePos<NpcId>,
156        actor: Actor,
157    ) {
158        self.state
159            .emit(OnMountVolume { actor, pos }, &mut (), world, index)
160    }
161
162    pub fn hook_pickup_owned_sprite(
163        &mut self,
164        world: &World,
165        index: IndexRef,
166        sprite: SpriteKind,
167        wpos: Vec3<i32>,
168        actor: Actor,
169    ) {
170        let site = world.sim().get(wpos.xy().wpos_to_cpos()).and_then(|chunk| {
171            chunk
172                .sites
173                .iter()
174                .find_map(|site| self.state.data().sites.world_site_map.get(site).copied())
175        });
176
177        self.state.emit(
178            OnTheft {
179                actor,
180                wpos,
181                sprite,
182                site,
183            },
184            &mut (),
185            world,
186            index,
187        )
188    }
189
190    pub fn hook_load_chunk(
191        &mut self,
192        key: Vec2<i32>,
193        max_res: EnumMap<ChunkResource, usize>,
194        world: &World,
195    ) {
196        if let Some(chunk_state) = self.state.get_resource_mut::<ChunkStates>().0.get_mut(key) {
197            *chunk_state = Some(LoadedChunkState { max_res });
198        }
199
200        if let Some(chunk) = world.sim().get(key) {
201            let data = self.state.get_data_mut();
202            for site in chunk.sites.iter() {
203                let Some(site) = data.sites.world_site_map.get(site) else {
204                    continue;
205                };
206
207                let site = *site;
208                let Some(site) = data.sites.get_mut(site) else {
209                    continue;
210                };
211
212                site.count_loaded_chunks += 1;
213            }
214        }
215    }
216
217    pub fn hook_unload_chunk(&mut self, key: Vec2<i32>, world: &World) {
218        if let Some(chunk_state) = self.state.get_resource_mut::<ChunkStates>().0.get_mut(key) {
219            *chunk_state = None;
220        }
221
222        if let Some(chunk) = world.sim().get(key) {
223            let data = self.state.get_data_mut();
224            for site in chunk.sites.iter() {
225                let Some(site) = data.sites.world_site_map.get(site) else {
226                    continue;
227                };
228
229                let site = *site;
230                let Some(site) = data.sites.get_mut(site) else {
231                    continue;
232                };
233
234                site.count_loaded_chunks = site.count_loaded_chunks.saturating_sub(1);
235            }
236        }
237    }
238
239    // Note that this hook only needs to be invoked if the block change results in a
240    // change to the rtsim resource produced by [`Block::get_rtsim_resource`].
241    pub fn hook_block_update(&mut self, world: &World, index: IndexRef, changes: Vec<BlockDiff>) {
242        self.state
243            .emit(event::OnBlockChange { changes }, &mut (), world, index);
244    }
245
246    pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) {
247        let data = self.state.get_data_mut();
248
249        if let Some(npc) = data.npcs.get_mut(entity.0) {
250            if matches!(npc.mode, SimulationMode::Simulated) {
251                error!("Unloaded already unloaded entity");
252            }
253            npc.mode = SimulationMode::Simulated;
254        }
255    }
256
257    pub fn hook_rtsim_actor_hp_change(
258        &mut self,
259        world: &World,
260        index: IndexRef,
261        actor: Actor,
262        cause: Option<Actor>,
263        new_hp_fraction: f32,
264        change: f32,
265    ) {
266        self.state.emit(
267            OnHealthChange {
268                actor,
269                cause,
270                new_health_fraction: new_hp_fraction,
271                change,
272            },
273            &mut (),
274            world,
275            index,
276        )
277    }
278
279    pub fn hook_rtsim_actor_death(
280        &mut self,
281        world: &World,
282        index: IndexRef,
283        actor: Actor,
284        wpos: Option<Vec3<f32>>,
285        killer: Option<Actor>,
286    ) {
287        self.state.emit(
288            OnDeath {
289                wpos,
290                actor,
291                killer,
292            },
293            &mut (),
294            world,
295            index,
296        );
297    }
298
299    pub fn hook_rtsim_actor_helped(
300        &mut self,
301        world: &World,
302        index: IndexRef,
303        actor: Actor,
304        saver: Option<Actor>,
305    ) {
306        self.state
307            .emit(OnHelped { actor, saver }, &mut (), world, index);
308    }
309
310    pub fn save(&mut self, wait_until_finished: bool) {
311        debug!("Saving rtsim data...");
312
313        // Create the save thread if it doesn't already exist
314        // We're not using the slow job pool here for two reasons:
315        // 1) The thread is mostly blocked on IO, not compute
316        // 2) We need to synchronise saves to ensure monotonicity, which slow jobs
317        // aren't designed to allow
318        let (tx, _) = self.save_thread.get_or_insert_with(|| {
319            trace!("Starting rtsim data save thread...");
320            let (tx, rx) = unbounded();
321            let file_path = self.file_path.clone();
322            (tx, thread::spawn(move || save_thread(file_path, rx)))
323        });
324
325        // Send rtsim data to the save thread
326        if let Err(err) = tx.send(self.state.data().clone()) {
327            error!("Failed to perform rtsim save: {}", err);
328        }
329
330        // If we need to wait until the save thread has done its work (due to, for
331        // example, server shutdown) then do that.
332        if wait_until_finished {
333            if let Some((tx, handle)) = self.save_thread.take() {
334                drop(tx);
335                info!("Waiting for rtsim save thread to finish...");
336                handle.join().expect("Save thread failed to join");
337                info!("Rtsim save thread finished.");
338            }
339        }
340
341        self.last_saved = Some(Instant::now());
342    }
343
344    // TODO: Clean up this API a bit
345    pub fn get_chunk_resources(&self, key: Vec2<i32>) -> EnumMap<ChunkResource, f32> {
346        self.state.data().nature.get_chunk_resources(key)
347    }
348
349    pub fn state(&self) -> &RtState { &self.state }
350
351    pub fn set_should_purge(&mut self, should_purge: bool) {
352        self.state.data_mut().should_purge = should_purge;
353    }
354}
355
356fn save_thread(file_path: PathBuf, rx: Receiver<Data>) {
357    if let Some(dir) = file_path.parent() {
358        let _ = fs::create_dir_all(dir);
359    }
360
361    let atomic_file = AtomicFile::new(file_path, OverwriteBehavior::AllowOverwrite);
362    while let Ok(data) = rx.recv() {
363        debug!("Writing rtsim data to file...");
364        match atomic_file.write(move |file| data.write_to(io::BufWriter::new(file))) {
365            Ok(_) => debug!("Rtsim data saved."),
366            Err(e) => error!("Saving rtsim data failed: {}", e),
367        }
368    }
369}
370
371pub struct ChunkStates(pub Grid<Option<LoadedChunkState>>);
372
373pub struct LoadedChunkState {
374    // The maximum possible number of each resource in this chunk
375    pub max_res: EnumMap<ChunkResource, usize>,
376}
377
378pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
379    dispatch::<tick::Sys>(dispatch_builder, &[&common_systems::phys::Sys::sys_name()]);
380}