Implement tiled multiplayer city simulation and scaling stages
This commit is contained in:
1 parent
3d4b29fedf
commit
3b1fcd996a
53 files changed
+7354
-13
No files matched your search
@@ -0,0 +1,94 @@
|
||||
use crate::{city::safe_id, protocol::RoomSave};
|
||||
use prost::Message;
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn save(root: &Path, snapshot: &RoomSave, keep: usize) -> Result<(), String> {
|
||||
if !safe_id(&snapshot.id) {
|
||||
return Err("Invalid room ID".into());
|
||||
}
|
||||
let folder = root.join(&snapshot.id);
|
||||
fs::create_dir_all(&folder).map_err(|e| e.to_string())?;
|
||||
let name = format!("{:020}.pb", snapshot.tick);
|
||||
let file = folder.join(&name);
|
||||
// Replace through a complete temporary file, including control changes that
|
||||
// happened between two ticks. Never truncate the last committed snapshot.
|
||||
{
|
||||
let temporary = folder.join(format!("{name}.tmp"));
|
||||
let mut output = fs::File::create(&temporary).map_err(|e| e.to_string())?;
|
||||
output
|
||||
.write_all(&snapshot.encode_to_vec())
|
||||
.map_err(|e| e.to_string())?;
|
||||
output.sync_all().map_err(|e| e.to_string())?;
|
||||
drop(output);
|
||||
fs::rename(temporary, file).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let mut files = snapshots(&folder)?;
|
||||
files.sort();
|
||||
let excess = files.len().saturating_sub(keep.max(1));
|
||||
for path in files.into_iter().take(excess) {
|
||||
fs::remove_file(path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn snapshots(folder: &Path) -> Result<Vec<PathBuf>, String> {
|
||||
Ok(fs::read_dir(folder)
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(Result::ok)
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|e| e == "pb"))
|
||||
.collect())
|
||||
}
|
||||
pub fn load(root: &Path, id: &str) -> Result<RoomSave, String> {
|
||||
if !safe_id(id) {
|
||||
return Err("Invalid room ID".into());
|
||||
}
|
||||
let mut files = snapshots(&root.join(id))?;
|
||||
files.sort();
|
||||
files.reverse();
|
||||
// Try an older intact file if the latest is damaged.
|
||||
for file in files {
|
||||
if let Ok(bytes) = fs::read(file) {
|
||||
if let Ok(snapshot) = RoomSave::decode(&bytes[..]) {
|
||||
if snapshot.id != id {
|
||||
continue;
|
||||
}
|
||||
if snapshot.version != 1 || snapshot.city_version != 1 {
|
||||
return Err("Incompatible room snapshot".into());
|
||||
}
|
||||
return Ok(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err("No compatible room snapshot".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn atomic_save_rotation_and_corrupt_fallback() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut s = RoomSave {
|
||||
version: 1,
|
||||
city_version: 1,
|
||||
id: "r".into(),
|
||||
..Default::default()
|
||||
};
|
||||
for tick in 0..5 {
|
||||
s.tick = tick;
|
||||
save(dir.path(), &s, 3).unwrap();
|
||||
}
|
||||
assert_eq!(snapshots(&dir.path().join("r")).unwrap().len(), 3);
|
||||
assert_eq!(load(dir.path(), "r").unwrap().tick, 4);
|
||||
s.speed = 4;
|
||||
save(dir.path(), &s, 3).unwrap();
|
||||
assert_eq!(load(dir.path(), "r").unwrap().speed, 4);
|
||||
fs::write(dir.path().join("r/00000000000000000004.pb"), [255, 255]).unwrap();
|
||||
assert_eq!(load(dir.path(), "r").unwrap().tick, 3);
|
||||
assert!(load(dir.path(), "../secret").is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user