From 8c8c70e99092eadd9958180714763846689a2f18 Mon Sep 17 00:00:00 2001 From: Serkyo Date: Thu, 9 Jul 2026 14:59:18 +0200 Subject: [PATCH] fix(shared): correct SYNR framing order and widen worldgen version --- crates/shared/src/save/region.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/shared/src/save/region.rs b/crates/shared/src/save/region.rs index 9f92afd..ae7b78c 100644 --- a/crates/shared/src/save/region.rs +++ b/crates/shared/src/save/region.rs @@ -44,19 +44,19 @@ pub struct RegionIndex { /// Monotonic cache-coherence counter for derived LOD tiles; only ever increases. region_tile_version: u32, /// The worldgen version most chunks in the region are pinned to; the stamp table holds only exceptions. - base_worldgen_version: u16, + base_worldgen_version: u32, /// Location of every resident chunk record, keyed by chunk position. header_table: BTreeMap, /// Reclaimable holes in the file, in no particular order. free_list: Vec, /// Worldgen-version exceptions: chunks pinned to a version other than `base_worldgen_version`. - stamps: BTreeMap, + stamps: BTreeMap, } impl RegionIndex { /// Creates an empty index whose chunks default to `base_worldgen_version`. #[must_use] - pub fn new(base_worldgen_version: u16) -> Self { + pub fn new(base_worldgen_version: u32) -> Self { Self { region_tile_version: 0, base_worldgen_version, @@ -79,7 +79,7 @@ impl RegionIndex { /// Returns the region's base worldgen version. #[must_use] - pub fn base_worldgen_version(&self) -> u16 { + pub fn base_worldgen_version(&self) -> u32 { self.base_worldgen_version } @@ -106,7 +106,7 @@ impl RegionIndex { /// Returns the pinned worldgen version for `pos`: its stamp exception, or the region base. #[must_use] - pub fn worldgen_version(&self, pos: ChunkPos) -> u16 { + pub fn worldgen_version(&self, pos: ChunkPos) -> u32 { self.stamps .get(&pos) .copied() @@ -114,7 +114,7 @@ impl RegionIndex { } /// Pins `pos` to `version`, recording it as a stamp exception only when it differs from the base. - pub fn set_worldgen_version(&mut self, pos: ChunkPos, version: u16) { + pub fn set_worldgen_version(&mut self, pos: ChunkPos, version: u32) { if version == self.base_worldgen_version { self.stamps.remove(&pos); } else { @@ -138,8 +138,8 @@ impl RegionIndex { let mut out = Vec::new(); out.extend_from_slice(&MAGIC); out.extend_from_slice(®ION_FORMAT_VERSION.to_le_bytes()); - out.extend_from_slice(&self.region_tile_version.to_le_bytes()); out.extend_from_slice(&self.base_worldgen_version.to_le_bytes()); + out.extend_from_slice(&self.region_tile_version.to_le_bytes()); out.extend_from_slice(&len_u32(self.header_table.len())?.to_le_bytes()); for (pos, entry) in &self.header_table { @@ -157,6 +157,8 @@ impl RegionIndex { out.extend_from_slice(&span.length.to_le_bytes()); } + // The stamp value is widened to u32 to match ChunkData's u32 worldgen version and avoid + // truncation; the spec's u16 stamp field (Save format.md) is treated as an oversight. out.extend_from_slice(&len_u32(self.stamps.len())?.to_le_bytes()); for (pos, version) in &self.stamps { out.extend_from_slice(&pos.x.to_le_bytes()); @@ -188,8 +190,8 @@ impl RegionIndex { }); } + let base_worldgen_version = u32::from_le_bytes(reader.take_array()?); let region_tile_version = u32::from_le_bytes(reader.take_array()?); - let base_worldgen_version = u16::from_le_bytes(reader.take_array()?); let header_len = u32::from_le_bytes(reader.take_array()?); let mut header_table = BTreeMap::new(); @@ -216,7 +218,7 @@ impl RegionIndex { let mut stamps = BTreeMap::new(); for _ in 0..stamp_len { let pos = read_pos(&mut reader)?; - stamps.insert(pos, u16::from_le_bytes(reader.take_array()?)); + stamps.insert(pos, u32::from_le_bytes(reader.take_array()?)); } Ok(Self { @@ -282,6 +284,18 @@ mod tests { Ok(()) } + #[test] + fn preserves_worldgen_versions_above_u16_max() -> Result<(), SaveError> { + // Regression guard: base and stamp worldgen versions are u32, so a value that would not + // fit a u16 must survive encode -> decode without truncation. + let mut index = RegionIndex::new(70_000); + index.set_worldgen_version(ChunkPos::new(0, 0, 0), 100_000); + let decoded = RegionIndex::decode(&index.encode()?)?; + assert_eq!(decoded.base_worldgen_version(), 70_000); + assert_eq!(decoded.worldgen_version(ChunkPos::new(0, 0, 0)), 100_000); + Ok(()) + } + #[test] fn stamp_equal_to_base_is_not_recorded() { let mut index = RegionIndex::new(7);