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.
|
/// 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>) {
|
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.
|
// 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() {
|
while let Ok(request) = request_rx.recv() {
|
||||||
match request {
|
match request {
|
||||||
SaveRequest::Read { pos, reply } => {
|
SaveRequest::Read { pos, reply } => {
|
||||||
|
|
@ -112,7 +112,7 @@ fn actor_loop(region_dir: &Path, request_rx: &Receiver<SaveRequest>) {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns the first [`SaveError`] produced by [`RegionFile::save`]; remaining dirty regions are still flushed.
|
/// 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(());
|
let mut result = Ok(());
|
||||||
for region in regions.values_mut() {
|
for region in regions.values_mut() {
|
||||||
// Clean regions are skipped so a flush never rewrites an unchanged file.
|
// 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.
|
/// Returns a [`SaveError`] from [`RegionFile::open`] if the region file exists but cannot be read or decoded.
|
||||||
fn region_mut<'a>(
|
fn region_mut<'a>(
|
||||||
regions: &'a mut HashMap<(i32, i32), RegionFile>,
|
regions: &'a mut HashMap<(i32, i32, i32), RegionFile>,
|
||||||
region_dir: &Path,
|
region_dir: &Path,
|
||||||
pos: ChunkPos,
|
pos: ChunkPos,
|
||||||
) -> Result<&'a mut RegionFile, SaveError> {
|
) -> 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.
|
// The region file is opened once on first touch; every later access hits the in-memory copy.
|
||||||
match regions.entry(key) {
|
match regions.entry(key) {
|
||||||
Entry::Occupied(entry) => Ok(entry.into_mut()),
|
Entry::Occupied(entry) => Ok(entry.into_mut()),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => Ok(entry.insert(RegionFile::open(region_path(
|
||||||
Ok(entry.insert(RegionFile::open(region_path(region_dir, pos.x, pos.z))?))
|
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.
|
/// Returns a [`SaveError`] if the region file cannot be opened or the stored record cannot be decoded.
|
||||||
fn read_chunk(
|
fn read_chunk(
|
||||||
regions: &mut HashMap<(i32, i32), RegionFile>,
|
regions: &mut HashMap<(i32, i32, i32), RegionFile>,
|
||||||
region_dir: &Path,
|
region_dir: &Path,
|
||||||
pos: ChunkPos,
|
pos: ChunkPos,
|
||||||
) -> Result<Option<ChunkData>, SaveError> {
|
) -> Result<Option<ChunkData>, SaveError> {
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,26 @@ use shared::save::record;
|
||||||
use shared::save::region::{HeaderEntry, RegionIndex};
|
use shared::save::region::{HeaderEntry, RegionIndex};
|
||||||
use shared::world::{ChunkData, ChunkPos};
|
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;
|
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]
|
#[must_use]
|
||||||
pub fn region_coords(cx: i32, cz: i32) -> (i32, i32) {
|
pub fn region_coords(cx: i32, cy: i32, cz: i32) -> (i32, i32, i32) {
|
||||||
(cx.div_euclid(REGION_SIZE), cz.div_euclid(REGION_SIZE))
|
(
|
||||||
|
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]
|
#[must_use]
|
||||||
pub fn region_path(dir: &Path, cx: i32, cz: i32) -> PathBuf {
|
pub fn region_path(dir: &Path, cx: i32, cy: i32, cz: i32) -> PathBuf {
|
||||||
let (rx, rz) = region_coords(cx, cz);
|
let (rx, ry, rz) = region_coords(cx, cy, cz);
|
||||||
dir.join(format!("r.{rx}.{rz}.region"))
|
dir.join(format!("r.{rx}.{ry}.{rz}.region"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An open region file: its `SYNR` index, the resident chunk records, and its on-disk location.
|
/// 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]
|
#[test]
|
||||||
fn region_coords_floor_negative_columns() {
|
fn region_coords_floor_negative_chunks() {
|
||||||
// Truncating division would map -1 to region 0; Euclidean flooring maps it to region -1.
|
// 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));
|
assert_eq!(region_coords(0, 0, 0), (0, 0, 0));
|
||||||
assert_eq!(region_coords(31, 31), (0, 0));
|
assert_eq!(region_coords(31, 31, 31), (0, 0, 0));
|
||||||
assert_eq!(region_coords(-1, -1), (-1, -1));
|
assert_eq!(region_coords(-1, -1, -1), (-1, -1, -1));
|
||||||
assert_eq!(region_coords(-32, -33), (-1, -2));
|
assert_eq!(region_coords(-32, -33, 64), (-1, -2, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn region_path_names_the_region_file() {
|
fn region_path_names_the_region_file() {
|
||||||
let dir = Path::new("/saves/world/region");
|
let dir = Path::new("/saves/world/region");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
region_path(dir, -1, 5),
|
region_path(dir, -1, 40, 5),
|
||||||
Path::new("/saves/world/region/r.-1.0.region")
|
Path::new("/saves/world/region/r.-1.1.0.region")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_missing_file_is_empty() -> Result<(), SaveError> {
|
fn open_missing_file_is_empty() -> Result<(), SaveError> {
|
||||||
let dir = tempfile::tempdir()?;
|
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!(region.is_empty());
|
||||||
assert_eq!(region.read_chunk(ChunkPos::new(0, 0, 0))?, None);
|
assert_eq!(region.read_chunk(ChunkPos::new(0, 0, 0))?, None);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -42,7 +42,7 @@ fn open_missing_file_is_empty() -> Result<(), SaveError> {
|
||||||
#[test]
|
#[test]
|
||||||
fn round_trips_chunks_through_disk() -> Result<(), SaveError> {
|
fn round_trips_chunks_through_disk() -> Result<(), SaveError> {
|
||||||
let dir = tempfile::tempdir()?;
|
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 = [
|
let positions = [
|
||||||
ChunkPos::new(0, 0, 0),
|
ChunkPos::new(0, 0, 0),
|
||||||
|
|
@ -72,7 +72,7 @@ fn round_trips_chunks_through_disk() -> Result<(), SaveError> {
|
||||||
#[test]
|
#[test]
|
||||||
fn record_offsets_are_valid_and_contiguous() -> Result<(), SaveError> {
|
fn record_offsets_are_valid_and_contiguous() -> Result<(), SaveError> {
|
||||||
let dir = tempfile::tempdir()?;
|
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())?;
|
let mut region = RegionFile::open(path.clone())?;
|
||||||
for pos in [
|
for pos in [
|
||||||
|
|
@ -99,7 +99,7 @@ fn record_offsets_are_valid_and_contiguous() -> Result<(), SaveError> {
|
||||||
#[test]
|
#[test]
|
||||||
fn remove_drops_only_the_named_chunk() -> Result<(), SaveError> {
|
fn remove_drops_only_the_named_chunk() -> Result<(), SaveError> {
|
||||||
let dir = tempfile::tempdir()?;
|
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 kept = ChunkPos::new(0, 0, 0);
|
||||||
let dropped = ChunkPos::new(1, 1, 1);
|
let dropped = ChunkPos::new(1, 1, 1);
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ fn remove_drops_only_the_named_chunk() -> Result<(), SaveError> {
|
||||||
#[test]
|
#[test]
|
||||||
fn stray_tmp_file_does_not_corrupt_reads() -> Result<(), SaveError> {
|
fn stray_tmp_file_does_not_corrupt_reads() -> Result<(), SaveError> {
|
||||||
let dir = tempfile::tempdir()?;
|
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 pos = ChunkPos::new(0, 0, 0);
|
||||||
|
|
||||||
let mut region = RegionFile::open(path.clone())?;
|
let mut region = RegionFile::open(path.clone())?;
|
||||||
|
|
@ -129,7 +129,7 @@ fn stray_tmp_file_does_not_corrupt_reads() -> Result<(), SaveError> {
|
||||||
region.save()?;
|
region.save()?;
|
||||||
|
|
||||||
// A leftover .tmp from an interrupted save must be ignored: only the renamed target is read.
|
// 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)?;
|
let reopened = RegionFile::open(path)?;
|
||||||
assert_eq!(reopened.read_chunk(pos)?, Some(sample(pos)));
|
assert_eq!(reopened.read_chunk(pos)?, Some(sample(pos)));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ fn saved_modification_is_applied_over_baseline() -> Result<(), SaveError> {
|
||||||
let mut data = ChunkData::new(pos, 0);
|
let mut data = ChunkData::new(pos, 0);
|
||||||
data.set(edited_index, edited_block);
|
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.write_chunk(pos, &data, 0)?;
|
||||||
region.save()?;
|
region.save()?;
|
||||||
|
|
||||||
|
|
@ -180,7 +180,7 @@ fn clean_chunk_is_not_written_back_on_eviction() -> Result<(), SaveError> {
|
||||||
flush(&world)?;
|
flush(&world)?;
|
||||||
|
|
||||||
// No record may exist for a chunk that never diverged from its baseline.
|
// 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());
|
assert!(region.read_chunk(pos)?.is_none());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue