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 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 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 if let Err(err) = tx.send(self.state.data().clone()) {
327 error!("Failed to perform rtsim save: {}", err);
328 }
329
330 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 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 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}