fix(shared): correct SYNR framing order and widen worldgen version

This commit is contained in:
Serkyo 2026-07-09 14:59:18 +02:00
parent de6f7dd722
commit 8c8c70e990

View file

@ -44,19 +44,19 @@ pub struct RegionIndex {
/// Monotonic cache-coherence counter for derived LOD tiles; only ever increases. /// Monotonic cache-coherence counter for derived LOD tiles; only ever increases.
region_tile_version: u32, region_tile_version: u32,
/// The worldgen version most chunks in the region are pinned to; the stamp table holds only exceptions. /// 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. /// Location of every resident chunk record, keyed by chunk position.
header_table: BTreeMap<ChunkPos, HeaderEntry>, header_table: BTreeMap<ChunkPos, HeaderEntry>,
/// Reclaimable holes in the file, in no particular order. /// Reclaimable holes in the file, in no particular order.
free_list: Vec<FreeSpan>, free_list: Vec<FreeSpan>,
/// Worldgen-version exceptions: chunks pinned to a version other than `base_worldgen_version`. /// Worldgen-version exceptions: chunks pinned to a version other than `base_worldgen_version`.
stamps: BTreeMap<ChunkPos, u16>, stamps: BTreeMap<ChunkPos, u32>,
} }
impl RegionIndex { impl RegionIndex {
/// Creates an empty index whose chunks default to `base_worldgen_version`. /// Creates an empty index whose chunks default to `base_worldgen_version`.
#[must_use] #[must_use]
pub fn new(base_worldgen_version: u16) -> Self { pub fn new(base_worldgen_version: u32) -> Self {
Self { Self {
region_tile_version: 0, region_tile_version: 0,
base_worldgen_version, base_worldgen_version,
@ -79,7 +79,7 @@ impl RegionIndex {
/// Returns the region's base worldgen version. /// Returns the region's base worldgen version.
#[must_use] #[must_use]
pub fn base_worldgen_version(&self) -> u16 { pub fn base_worldgen_version(&self) -> u32 {
self.base_worldgen_version self.base_worldgen_version
} }
@ -106,7 +106,7 @@ impl RegionIndex {
/// Returns the pinned worldgen version for `pos`: its stamp exception, or the region base. /// Returns the pinned worldgen version for `pos`: its stamp exception, or the region base.
#[must_use] #[must_use]
pub fn worldgen_version(&self, pos: ChunkPos) -> u16 { pub fn worldgen_version(&self, pos: ChunkPos) -> u32 {
self.stamps self.stamps
.get(&pos) .get(&pos)
.copied() .copied()
@ -114,7 +114,7 @@ impl RegionIndex {
} }
/// Pins `pos` to `version`, recording it as a stamp exception only when it differs from the base. /// 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 { if version == self.base_worldgen_version {
self.stamps.remove(&pos); self.stamps.remove(&pos);
} else { } else {
@ -138,8 +138,8 @@ impl RegionIndex {
let mut out = Vec::new(); let mut out = Vec::new();
out.extend_from_slice(&MAGIC); out.extend_from_slice(&MAGIC);
out.extend_from_slice(&REGION_FORMAT_VERSION.to_le_bytes()); out.extend_from_slice(&REGION_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.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()); out.extend_from_slice(&len_u32(self.header_table.len())?.to_le_bytes());
for (pos, entry) in &self.header_table { for (pos, entry) in &self.header_table {
@ -157,6 +157,8 @@ impl RegionIndex {
out.extend_from_slice(&span.length.to_le_bytes()); 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()); out.extend_from_slice(&len_u32(self.stamps.len())?.to_le_bytes());
for (pos, version) in &self.stamps { for (pos, version) in &self.stamps {
out.extend_from_slice(&pos.x.to_le_bytes()); 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 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 header_len = u32::from_le_bytes(reader.take_array()?);
let mut header_table = BTreeMap::new(); let mut header_table = BTreeMap::new();
@ -216,7 +218,7 @@ impl RegionIndex {
let mut stamps = BTreeMap::new(); let mut stamps = BTreeMap::new();
for _ in 0..stamp_len { for _ in 0..stamp_len {
let pos = read_pos(&mut reader)?; 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 { Ok(Self {
@ -282,6 +284,18 @@ mod tests {
Ok(()) 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] #[test]
fn stamp_equal_to_base_is_not_recorded() { fn stamp_equal_to_base_is_not_recorded() {
let mut index = RegionIndex::new(7); let mut index = RegionIndex::new(7);