refactor(server): move region files to 3D (rx, ry, rz) grid
This commit is contained in:
parent
b3f10d8f12
commit
25b088ce0b
|
|
@ -73,7 +73,7 @@ impl SaveActor {
|
|||
/// The actor's run loop: it owns the region-file map and answers requests until every sender is dropped.
|
||||
fn actor_loop(region_dir: &Path, request_rx: &Receiver<SaveRequest>) {
|
||||
// The actor is the sole owner of this map, so region files need no lock of their own.
|
||||
let mut regions: HashMap<(i32, i32), RegionFile> = HashMap::new();
|
||||
let mut regions: HashMap<(i32, i32, i32), RegionFile> = HashMap::new();
|
||||
while let Ok(request) = request_rx.recv() {
|
||||
match request {
|
||||
SaveRequest::Read { pos, reply } => {
|
||||
|
|
@ -112,7 +112,7 @@ fn actor_loop(region_dir: &Path, request_rx: &Receiver<SaveRequest>) {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns the first [`SaveError`] produced by [`RegionFile::save`]; remaining dirty regions are still flushed.
|
||||
fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), SaveError> {
|
||||
fn flush_dirty(regions: &mut HashMap<(i32, i32, i32), RegionFile>) -> Result<(), SaveError> {
|
||||
let mut result = Ok(());
|
||||
for region in regions.values_mut() {
|
||||
// Clean regions are skipped so a flush never rewrites an unchanged file.
|
||||
|
|
@ -136,17 +136,17 @@ fn flush_dirty(regions: &mut HashMap<(i32, i32), RegionFile>) -> Result<(), Save
|
|||
///
|
||||
/// Returns a [`SaveError`] from [`RegionFile::open`] if the region file exists but cannot be read or decoded.
|
||||
fn region_mut<'a>(
|
||||
regions: &'a mut HashMap<(i32, i32), RegionFile>,
|
||||
regions: &'a mut HashMap<(i32, i32, i32), RegionFile>,
|
||||
region_dir: &Path,
|
||||
pos: ChunkPos,
|
||||
) -> Result<&'a mut RegionFile, SaveError> {
|
||||
let key = region_coords(pos.x, pos.z);
|
||||
let key = region_coords(pos.x, pos.y, pos.z);
|
||||
// The region file is opened once on first touch; every later access hits the in-memory copy.
|
||||
match regions.entry(key) {
|
||||
Entry::Occupied(entry) => Ok(entry.into_mut()),
|
||||
Entry::Vacant(entry) => {
|
||||
Ok(entry.insert(RegionFile::open(region_path(region_dir, pos.x, pos.z))?))
|
||||
}
|
||||
Entry::Vacant(entry) => Ok(entry.insert(RegionFile::open(region_path(
|
||||
region_dir, pos.x, pos.y, pos.z,
|
||||
))?)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ fn region_mut<'a>(
|
|||
///
|
||||
/// Returns a [`SaveError`] if the region file cannot be opened or the stored record cannot be decoded.
|
||||
fn read_chunk(
|
||||
regions: &mut HashMap<(i32, i32), RegionFile>,
|
||||
regions: &mut HashMap<(i32, i32, i32), RegionFile>,
|
||||
region_dir: &Path,
|
||||
pos: ChunkPos,
|
||||
) -> Result<Option<ChunkData>, SaveError> {
|
||||
|
|
|
|||
|
|
@ -13,20 +13,26 @@ use shared::save::record;
|
|||
use shared::save::region::{HeaderEntry, RegionIndex};
|
||||
use shared::world::{ChunkData, ChunkPos};
|
||||
|
||||
/// The side length, in chunk columns, of the square footprint one region file covers.
|
||||
/// The side length, in chunks, of the cube one region file covers on every axis.
|
||||
pub const REGION_SIZE: i32 = 32;
|
||||
|
||||
/// Maps a chunk column `(cx, cz)` to the coordinates `(rx, rz)` of the region that contains it.
|
||||
/// Maps a chunk `(cx, cy, cz)` to the coordinates `(rx, ry, rz)` of the region cube that contains it.
|
||||
///
|
||||
/// Every axis is floored via `div_euclid` (not truncating division) so negative chunk coordinates map to the region below rather than toward zero: chunk `-1` belongs to region `-1`, not region `0`.
|
||||
#[must_use]
|
||||
pub fn region_coords(cx: i32, cz: i32) -> (i32, i32) {
|
||||
(cx.div_euclid(REGION_SIZE), cz.div_euclid(REGION_SIZE))
|
||||
pub fn region_coords(cx: i32, cy: i32, cz: i32) -> (i32, i32, i32) {
|
||||
(
|
||||
cx.div_euclid(REGION_SIZE),
|
||||
cy.div_euclid(REGION_SIZE),
|
||||
cz.div_euclid(REGION_SIZE),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the on-disk path of the region file containing chunk column `(cx, cz)` within `dir`.
|
||||
/// Builds the on-disk path of the region file containing chunk `(cx, cy, cz)` within `dir`.
|
||||
#[must_use]
|
||||
pub fn region_path(dir: &Path, cx: i32, cz: i32) -> PathBuf {
|
||||
let (rx, rz) = region_coords(cx, cz);
|
||||
dir.join(format!("r.{rx}.{rz}.region"))
|
||||
pub fn region_path(dir: &Path, cx: i32, cy: i32, cz: i32) -> PathBuf {
|
||||
let (rx, ry, rz) = region_coords(cx, cy, cz);
|
||||
dir.join(format!("r.{rx}.{ry}.{rz}.region"))
|
||||
}
|
||||
|
||||
/// An open region file: its `SYNR` index, the resident chunk records, and its on-disk location.
|
||||
|
|
|
|||
|
|
@ -13,27 +13,27 @@ fn sample(pos: ChunkPos) -> ChunkData {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn region_coords_floor_negative_columns() {
|
||||
// Truncating division would map -1 to region 0; Euclidean flooring maps it to region -1.
|
||||
assert_eq!(region_coords(0, 0), (0, 0));
|
||||
assert_eq!(region_coords(31, 31), (0, 0));
|
||||
assert_eq!(region_coords(-1, -1), (-1, -1));
|
||||
assert_eq!(region_coords(-32, -33), (-1, -2));
|
||||
fn region_coords_floor_negative_chunks() {
|
||||
// Truncating division would map -1 to region 0; Euclidean flooring maps it to region -1. Every axis, including Y, floors identically under the 3D region grid.
|
||||
assert_eq!(region_coords(0, 0, 0), (0, 0, 0));
|
||||
assert_eq!(region_coords(31, 31, 31), (0, 0, 0));
|
||||
assert_eq!(region_coords(-1, -1, -1), (-1, -1, -1));
|
||||
assert_eq!(region_coords(-32, -33, 64), (-1, -2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_path_names_the_region_file() {
|
||||
let dir = Path::new("/saves/world/region");
|
||||
assert_eq!(
|
||||
region_path(dir, -1, 5),
|
||||
Path::new("/saves/world/region/r.-1.0.region")
|
||||
region_path(dir, -1, 40, 5),
|
||||
Path::new("/saves/world/region/r.-1.1.0.region")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_missing_file_is_empty() -> Result<(), SaveError> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let region = RegionFile::open(dir.path().join("r.0.0.region"))?;
|
||||
let region = RegionFile::open(dir.path().join("r.0.0.0.region"))?;
|
||||
assert!(region.is_empty());
|
||||
assert_eq!(region.read_chunk(ChunkPos::new(0, 0, 0))?, None);
|
||||
Ok(())
|
||||
|
|
@ -42,7 +42,7 @@ fn open_missing_file_is_empty() -> Result<(), SaveError> {
|
|||
#[test]
|
||||
fn round_trips_chunks_through_disk() -> Result<(), SaveError> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path().join("r.0.0.region");
|
||||
let path = dir.path().join("r.0.0.0.region");
|
||||
|
||||
let positions = [
|
||||
ChunkPos::new(0, 0, 0),
|
||||
|
|
@ -72,7 +72,7 @@ fn round_trips_chunks_through_disk() -> Result<(), SaveError> {
|
|||
#[test]
|
||||
fn record_offsets_are_valid_and_contiguous() -> Result<(), SaveError> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path().join("r.0.0.region");
|
||||
let path = dir.path().join("r.0.0.0.region");
|
||||
|
||||
let mut region = RegionFile::open(path.clone())?;
|
||||
for pos in [
|
||||
|
|
@ -99,7 +99,7 @@ fn record_offsets_are_valid_and_contiguous() -> Result<(), SaveError> {
|
|||
#[test]
|
||||
fn remove_drops_only_the_named_chunk() -> Result<(), SaveError> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path().join("r.0.0.region");
|
||||
let path = dir.path().join("r.0.0.0.region");
|
||||
let kept = ChunkPos::new(0, 0, 0);
|
||||
let dropped = ChunkPos::new(1, 1, 1);
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ fn remove_drops_only_the_named_chunk() -> Result<(), SaveError> {
|
|||
#[test]
|
||||
fn stray_tmp_file_does_not_corrupt_reads() -> Result<(), SaveError> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path().join("r.0.0.region");
|
||||
let path = dir.path().join("r.0.0.0.region");
|
||||
let pos = ChunkPos::new(0, 0, 0);
|
||||
|
||||
let mut region = RegionFile::open(path.clone())?;
|
||||
|
|
@ -129,7 +129,7 @@ fn stray_tmp_file_does_not_corrupt_reads() -> Result<(), SaveError> {
|
|||
region.save()?;
|
||||
|
||||
// A leftover .tmp from an interrupted save must be ignored: only the renamed target is read.
|
||||
fs::write(dir.path().join("r.0.0.region.tmp"), b"garbage")?;
|
||||
fs::write(dir.path().join("r.0.0.0.region.tmp"), b"garbage")?;
|
||||
let reopened = RegionFile::open(path)?;
|
||||
assert_eq!(reopened.read_chunk(pos)?, Some(sample(pos)));
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ fn saved_modification_is_applied_over_baseline() -> Result<(), SaveError> {
|
|||
let mut data = ChunkData::new(pos, 0);
|
||||
data.set(edited_index, edited_block);
|
||||
|
||||
let mut region = RegionFile::open(region_path(dir.path(), pos.x, pos.z))?;
|
||||
let mut region = RegionFile::open(region_path(dir.path(), pos.x, pos.y, pos.z))?;
|
||||
region.write_chunk(pos, &data, 0)?;
|
||||
region.save()?;
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ fn clean_chunk_is_not_written_back_on_eviction() -> Result<(), SaveError> {
|
|||
flush(&world)?;
|
||||
|
||||
// No record may exist for a chunk that never diverged from its baseline.
|
||||
let region = RegionFile::open(region_path(dir.path(), pos.x, pos.z))?;
|
||||
let region = RegionFile::open(region_path(dir.path(), pos.x, pos.y, pos.z))?;
|
||||
assert!(region.read_chunk(pos)?.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue