Implement tiled multiplayer city simulation and scaling stages

This commit is contained in:
mrleo1nid committed 2026-10-30 22:06:56 +03:00
1 parent 3d4b29fedf
commit 3b1fcd996a
53 files changed
+7354 -13

No files matched your search

+94
View File
@@ -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());
}
}