Skip to main content

steel_core/chunk_saver/
format.rs

1//! Data structures for the chunk persistence format.
2//!
3//! ## Format Overview
4//!
5//! Region files use a sector-based format with a fixed header for fast random access:
6//!
7//! ```text
8//! ┌─────────────────────────────────────────────────────┐
9//! │ Magic (4 bytes): "STLR"                             │
10//! │ Version (2 bytes): u16                              │
11//! │ Padding (2 bytes): reserved                         │
12//! ├─────────────────────────────────────────────────────┤
13//! │ Header: 1024 entries × 8 bytes = 8KB                │
14//! │   Each entry: offset (u32) + size (u24) + flags (u8)│
15//! ├─────────────────────────────────────────────────────┤
16//! │ Chunk data in 4KB sectors                           │
17//! │   [chunk data padded to 4KB boundary]               │
18//! │   [chunk data padded to 4KB boundary]               │
19//! │   ...                                               │
20//! └─────────────────────────────────────────────────────┘
21//! ```
22//!
23//! ## Design
24//!
25//! Each chunk stores its own block state and biome palettes, making chunks
26//! self-contained and avoiding expensive region-wide table rebuilds.
27//!
28//! Block data uses power-of-2 bit packing (1, 2, 4, 8, 16 bits) to avoid entries
29//! spanning u64 boundaries.
30
31use glam::IVec3;
32use steel_utils::{BoundingBox, Identifier, PackedChunkPos};
33use wincode::{SchemaRead, SchemaWrite};
34
35use crate::chunk::status::ChunkStatus;
36
37/// Magic bytes for region file identification: "STLR" (Steel Region)
38pub const REGION_MAGIC: [u8; 4] = *b"STLR";
39
40/// Current format version. Increment when making breaking changes.
41/// v3: Added entity persistence (`PersistentEntity`).
42/// v4: Added scheduled tick persistence (`PersistentTick`).
43/// v5: Added heightmap persistence (`PersistentHeightmap`).
44/// v6: Added structure start and structure reference persistence.
45/// v7: Added POI persistence (`PersistentPoi`).
46/// v8: Added typed jigsaw piece-state persistence.
47/// v9: Added proto chunk carving mask persistence and typed packed chunk references.
48/// v10: Added template piece clip and postprocess persistence.
49/// v11: Added template piece placement adjustment persistence.
50/// v12: Added igloo template marker, placement adjustment, and postprocess persistence.
51/// v13: Split template processor persistence and added ruined-portal processors.
52/// v14: Added buried treasure procedural piece persistence.
53/// v15: Added procedural structure-piece payload persistence.
54/// v16: Added entity fall distance persistence.
55/// v17: Added entity `NoGravity` persistence.
56/// v18: Added entity `Invulnerable` persistence.
57/// v19: Added shared entity save-data persistence.
58/// v20: Added chunk-owned light section persistence.
59/// v21: Matched vanilla scheduled-tick persistence by rebuilding sub-tick order on load.
60/// v22: Preserve Vanilla pending `DUMMY` block entities across chunk stages.
61pub const FORMAT_VERSION: u16 = 22;
62
63/// Number of chunks per region side (32×32 = 1024 chunks per region).
64pub const REGION_SIZE: usize = 32;
65
66/// Total chunks in a region.
67pub const CHUNKS_PER_REGION: usize = REGION_SIZE * REGION_SIZE;
68
69/// Number of blocks per section side (16×16×16 = 4096 blocks per section).
70pub const SECTION_SIZE: usize = 16;
71
72/// Total blocks in a section.
73pub const BLOCKS_PER_SECTION: usize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;
74
75/// Number of biome cells per section side (4×4×4 = 64 biomes per section).
76pub const BIOME_SIZE: usize = 4;
77
78/// Total biome cells in a section.
79pub const BIOMES_PER_SECTION: usize = BIOME_SIZE * BIOME_SIZE * BIOME_SIZE;
80
81/// Sector size in bytes (4KB, matches modern disk physical sectors).
82pub const SECTOR_SIZE: usize = 4096;
83
84/// Size of the file header (magic + version + padding).
85pub const FILE_HEADER_SIZE: usize = 8;
86
87/// Size of the chunk location table (1024 entries × 8 bytes).
88pub const CHUNK_TABLE_SIZE: usize = CHUNKS_PER_REGION * 8;
89
90/// Total header size (file header + chunk table).
91pub const TOTAL_HEADER_SIZE: usize = FILE_HEADER_SIZE + CHUNK_TABLE_SIZE;
92
93/// First sector where chunk data can be stored.
94/// Header takes `ceil(TOTAL_HEADER_SIZE` / `SECTOR_SIZE`) = 3 sectors (8 + 8192 = 8200 bytes).
95pub const FIRST_DATA_SECTOR: u32 = 3;
96
97/// Maximum chunk size in bytes (16MB - should be plenty).
98pub const MAX_CHUNK_SIZE: usize = 16 * 1024 * 1024;
99
100/// Entry in the chunk location table.
101///
102/// Layout (8 bytes total):
103/// - offset: u32 - sector offset (0 = chunk doesn't exist)
104/// - size: u24 - compressed size in bytes
105/// - flags: u8 - status and flags
106#[derive(Clone, Copy, PartialEq, Eq)]
107pub struct ChunkEntry {
108    /// Sector offset from start of file. 0 means chunk doesn't exist.
109    /// Multiply by `SECTOR_SIZE` to get byte offset.
110    pub sector_offset: u32,
111    /// Size of compressed chunk data in bytes (stored as u24, max ~16MB).
112    pub size_bytes: u32,
113    /// Chunk status (generation state).
114    pub status: ChunkStatus,
115}
116
117impl ChunkEntry {
118    /// Creates a new chunk entry.
119    #[must_use]
120    pub const fn new(sector_offset: u32, size_bytes: u32, status: ChunkStatus) -> Self {
121        Self {
122            sector_offset,
123            size_bytes,
124            status,
125        }
126    }
127
128    /// Returns true if this entry represents an existing chunk.
129    #[must_use]
130    pub const fn exists(&self) -> bool {
131        self.sector_offset != 0
132    }
133
134    /// Creates an empty/non-existent chunk entry.
135    #[must_use]
136    pub const fn empty() -> Self {
137        Self {
138            sector_offset: 0,
139            size_bytes: 0,
140            status: ChunkStatus::Empty,
141        }
142    }
143
144    /// Calculates the number of sectors this chunk occupies.
145    #[must_use]
146    pub const fn sector_count(&self) -> u32 {
147        if self.size_bytes == 0 {
148            0
149        } else {
150            (self.size_bytes as usize).div_ceil(SECTOR_SIZE) as u32
151        }
152    }
153
154    /// Serializes to 8 bytes: [offset: 4][size: 3][flags: 1]
155    #[must_use]
156    pub const fn to_bytes(self) -> [u8; 8] {
157        let offset_bytes = self.sector_offset.to_le_bytes();
158        let size_bytes = self.size_bytes.to_le_bytes();
159        let flags = self.status.get_index() as u8;
160        [
161            offset_bytes[0],
162            offset_bytes[1],
163            offset_bytes[2],
164            offset_bytes[3],
165            size_bytes[0],
166            size_bytes[1],
167            size_bytes[2],
168            flags,
169        ]
170    }
171
172    /// Deserializes from 8 bytes.
173    #[must_use]
174    pub fn from_bytes(bytes: [u8; 8]) -> Option<Self> {
175        let sector_offset = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
176        let size_bytes = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], 0]);
177        let status = if sector_offset == 0 {
178            ChunkStatus::Empty
179        } else {
180            ChunkStatus::from_index(bytes[7] as usize)?
181        };
182        Some(Self {
183            sector_offset,
184            size_bytes,
185            status,
186        })
187    }
188}
189
190impl Default for ChunkEntry {
191    fn default() -> Self {
192        Self::empty()
193    }
194}
195
196/// Region header containing chunk location table.
197pub struct RegionHeader {
198    /// Chunk entries (1024 = 32×32).
199    pub entries: Box<[ChunkEntry; CHUNKS_PER_REGION]>,
200}
201
202impl RegionHeader {
203    /// Creates an empty header with no chunks.
204    #[must_use]
205    pub fn new() -> Self {
206        Self {
207            entries: Box::new([ChunkEntry::default(); CHUNKS_PER_REGION]),
208        }
209    }
210
211    /// Gets the local index for a chunk position within this region.
212    #[must_use]
213    pub const fn chunk_index(local_x: usize, local_z: usize) -> usize {
214        debug_assert!(local_x < REGION_SIZE);
215        debug_assert!(local_z < REGION_SIZE);
216        local_z * REGION_SIZE + local_x
217    }
218
219    /// Converts a chunk index back to local coordinates.
220    #[must_use]
221    pub const fn index_to_local(index: usize) -> (usize, usize) {
222        debug_assert!(index < CHUNKS_PER_REGION);
223        (index % REGION_SIZE, index / REGION_SIZE)
224    }
225
226    /// Serializes the header to bytes.
227    #[must_use]
228    pub fn to_bytes(&self) -> Vec<u8> {
229        let mut bytes = Vec::with_capacity(CHUNK_TABLE_SIZE);
230        for entry in &self.entries {
231            bytes.extend_from_slice(&entry.to_bytes());
232        }
233        bytes
234    }
235
236    /// Deserializes the header from bytes.
237    ///
238    /// # Panics
239    /// Panics if bytes length is not exactly `CHUNK_TABLE_SIZE`.
240    pub fn from_bytes(bytes: &[u8]) -> Result<Self, usize> {
241        assert_eq!(bytes.len(), CHUNK_TABLE_SIZE);
242        let mut entries = Box::new([ChunkEntry::default(); CHUNKS_PER_REGION]);
243        for (i, &chunk) in bytes.as_chunks::<8>().0.iter().enumerate() {
244            let Some(entry) = ChunkEntry::from_bytes(chunk) else {
245                return Err(i);
246            };
247            entries[i] = entry;
248        }
249        Ok(Self { entries })
250    }
251
252    /// Finds a contiguous range of free sectors for allocation.
253    ///
254    /// Returns the starting sector offset, or `None` if no suitable range exists.
255    #[must_use]
256    pub fn find_free_sectors(&self, sectors_needed: u32, file_sectors: u32) -> u32 {
257        if sectors_needed == 0 {
258            return FIRST_DATA_SECTOR;
259        }
260
261        // Build a list of (start, end) ranges for used sectors
262        let mut used_ranges: Vec<(u32, u32)> = self
263            .entries
264            .iter()
265            .filter(|e| e.exists())
266            .map(|e| (e.sector_offset, e.sector_offset + e.sector_count()))
267            .collect();
268        used_ranges.sort_by_key(|r| r.0);
269
270        // Try to find a gap between used ranges
271        let mut current_sector = FIRST_DATA_SECTOR;
272        for (start, end) in used_ranges {
273            if start >= current_sector + sectors_needed {
274                // Found a gap
275                return current_sector;
276            }
277            current_sector = current_sector.max(end);
278        }
279
280        // No gap found, append at the end
281        current_sector.max(file_sectors)
282    }
283}
284
285impl Default for RegionHeader {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291/// A block state with its identifier and properties.
292#[derive(SchemaWrite, SchemaRead, Clone, PartialEq, Eq, Hash, Debug)]
293pub struct PersistentBlockState<'a> {
294    /// Block identifier (e.g., "`minecraft:oak_stairs`").
295    pub name: Identifier,
296    /// Block properties as key-value pairs (e.g., [("facing", "north")]).
297    pub properties: Vec<(&'a str, &'a str)>,
298}
299
300/// A heightmap stored with a chunk.
301///
302/// Height values are stored relative to `min_y` (same as the runtime `Heightmap`).
303/// Type discriminants: 0=WorldSurface, 1=MotionBlocking, 2=MotionBlockingNoLeaves, 3=OceanFloor,
304/// 4=WorldSurfaceWg, 5=OceanFloorWg.
305#[derive(SchemaWrite, SchemaRead)]
306pub struct PersistentHeightmap {
307    /// Heightmap type discriminant.
308    pub heightmap_type: u8,
309    /// 256 height values (one per column), stored relative to `min_y`.
310    pub data: Vec<u16>,
311}
312
313/// Chunk-owned light data stored with a chunk.
314#[derive(SchemaWrite, SchemaRead, Default)]
315pub struct PersistentLightData {
316    /// Block-light sections indexed by light-section index.
317    pub block: Vec<PersistentLightSection>,
318    /// Sky-light sections indexed by light-section index.
319    pub sky: Vec<PersistentLightSection>,
320}
321
322/// One persisted chunk-owned light section.
323#[derive(SchemaWrite, SchemaRead)]
324pub enum PersistentLightSection {
325    /// Present zero-filled section without backing bytes.
326    Uninitialized {
327        /// Index in the chunk's padded light-section array.
328        section_index: u32,
329    },
330    /// Visible initialized light bytes.
331    Initialized {
332        /// Index in the chunk's padded light-section array.
333        section_index: u32,
334        /// Packed 4-bit light values.
335        data: Vec<u8>,
336    },
337    /// Initialized bytes hidden from vanilla packet conversion.
338    Internal {
339        /// Index in the chunk's padded light-section array.
340        section_index: u32,
341        /// Packed 4-bit light values.
342        data: Vec<u8>,
343    },
344}
345
346impl PersistentLightSection {
347    /// Returns the padded light-section index.
348    #[must_use]
349    pub const fn section_index(&self) -> u32 {
350        match self {
351            Self::Uninitialized { section_index }
352            | Self::Initialized { section_index, .. }
353            | Self::Internal { section_index, .. } => *section_index,
354        }
355    }
356}
357
358/// A persistent chunk containing sections and metadata.
359///
360/// Each chunk stores its own block state and biome palettes, making it
361/// self-contained. Sections reference indices into these chunk-level palettes.
362#[derive(SchemaWrite, SchemaRead)]
363pub struct PersistentChunk<'a> {
364    /// Unix timestamp of last modification.
365    pub last_modified: u32,
366    /// Block states used in this chunk. Sections reference indices into this.
367    pub block_states: Vec<PersistentBlockState<'a>>,
368    /// Biomes used in this chunk. Sections reference indices into this.
369    pub biomes: Vec<Identifier>,
370    /// Vertical sections (typically 24 for -64 to 319).
371    pub sections: Vec<PersistentSection>,
372    /// Block entities (chests, signs, etc.).
373    pub block_entities: Vec<PersistentBlockEntity>,
374    /// Entities in this chunk (excludes players and non-serializable types).
375    pub entities: Vec<PersistentEntity>,
376    /// Scheduled block ticks pending in this chunk.
377    pub block_ticks: Vec<PersistentTick>,
378    /// Scheduled fluid ticks pending in this chunk.
379    pub fluid_ticks: Vec<PersistentTick>,
380    /// Materialized heightmaps allowed by the chunk's persisted status.
381    pub heightmaps: Vec<PersistentHeightmap>,
382    /// Chunk-owned light sections.
383    pub light: PersistentLightData,
384    /// Proto chunk carving mask as Steel's packed bitset layout.
385    pub carving_mask: Option<Vec<u64>>,
386    /// Pending postprocessing offsets grouped by section index.
387    pub postprocessing: Vec<Vec<u16>>,
388    /// Structure starts originating in this chunk.
389    pub structure_starts: Vec<PersistentStructureStart>,
390    /// References to structures from nearby origin chunks.
391    pub structure_references: Vec<PersistentStructureReference>,
392    /// POI occupancy data (ticket state for beds, workstations, etc.).
393    pub pois: Vec<PersistentPoi>,
394}
395
396/// A 16×16×16 section of a chunk.
397#[derive(SchemaWrite, SchemaRead)]
398pub enum PersistentSection {
399    /// All blocks are the same type.
400    Homogeneous {
401        /// Index into chunk's `block_states` palette.
402        block_state: u16,
403        /// Biome data for this section.
404        biomes: PersistentBiomeData,
405    },
406    /// Multiple block types present.
407    Heterogeneous {
408        /// Section-local palette: indices into chunk's `block_states` palette.
409        palette: Vec<u16>,
410        /// Bits per entry (1, 2, 4, 8, or 16).
411        bits_per_entry: u8,
412        /// Packed block indices into section-local palette. 4096 entries.
413        block_data: Box<[u64]>,
414        /// Biome data for this section.
415        biomes: PersistentBiomeData,
416    },
417}
418
419/// Biome data for a section (4×4×4 = 64 cells).
420#[derive(SchemaWrite, SchemaRead)]
421pub enum PersistentBiomeData {
422    /// All 64 biome cells are the same.
423    Homogeneous {
424        /// Index into chunk's `biomes` palette.
425        biome: u16,
426    },
427    /// Multiple biomes present.
428    Heterogeneous {
429        /// Section-local palette: indices into chunk's `biomes` palette.
430        palette: Vec<u16>,
431        /// Bits per entry (1, 2, 4, or 8).
432        bits_per_entry: u8,
433        /// Packed biome indices into section-local palette. 64 entries.
434        biome_data: Box<[u64]>,
435    },
436}
437
438/// A block entity (tile entity) stored with a chunk.
439///
440/// Block entities are serialized with their type and NBT data.
441/// The NBT data is stored as raw bytes (simdnbt binary format).
442#[derive(SchemaWrite, SchemaRead)]
443pub struct PersistentBlockEntity {
444    /// Relative X position within chunk (0-15).
445    pub x: u8,
446    /// Absolute Y position (world height).
447    pub y: i16,
448    /// Relative Z position within chunk (0-15).
449    pub z: u8,
450    /// Block entity type identifier, or `None` for Vanilla's pending `DUMMY` marker.
451    pub entity_type: Option<Identifier>,
452    /// Serialized NBT data (simdnbt binary format).
453    /// Contains the block entity's custom data from `save_additional`.
454    pub nbt_data: Vec<u8>,
455}
456
457/// An entity stored with a chunk.
458///
459/// Unlike vanilla which stores entities in separate region files,
460/// Steel stores entities inline with chunk data for simplicity.
461/// Base entity fields are stored directly; type-specific data is in `nbt_data`.
462#[derive(Debug, Clone, SchemaWrite, SchemaRead)]
463pub struct PersistentEntity {
464    /// Entity type identifier (e.g., "minecraft:item").
465    pub entity_type: Identifier,
466    /// Persistent UUID (16 bytes).
467    pub uuid: [u8; 16],
468    /// Position (x, y, z) in absolute world coordinates.
469    pub pos: [f64; 3],
470    /// Velocity (x, y, z) in blocks per tick.
471    pub motion: [f64; 3],
472    /// Rotation (yaw, pitch) in degrees.
473    pub rotation: [f32; 2],
474    /// Accumulated vanilla fall distance.
475    pub fall_distance: f64,
476    /// Vanilla `remainingFireTicks`.
477    pub remaining_fire_ticks: i32,
478    /// Synchronized vanilla `TicksFrozen`.
479    pub ticks_frozen: i32,
480    /// Vanilla `isInPowderSnow`.
481    pub is_in_powder_snow: bool,
482    /// Vanilla `wasInPowderSnow`.
483    pub was_in_powder_snow: bool,
484    /// Vanilla `hasVisualFire`.
485    pub has_visual_fire: bool,
486    /// Whether entity is on ground.
487    pub on_ground: bool,
488    /// Shared vanilla `NoGravity` flag.
489    pub no_gravity: bool,
490    /// Shared vanilla `Invulnerable` flag.
491    pub invulnerable: bool,
492    /// Synchronized vanilla `Air` value.
493    pub air_supply: i32,
494    /// Vanilla dimension-change portal cooldown.
495    pub portal_cooldown: i32,
496    /// Optional vanilla custom name stored as a root compound containing `CustomName`.
497    pub custom_name_nbt: Vec<u8>,
498    /// Synchronized vanilla custom-name visibility flag.
499    pub custom_name_visible: bool,
500    /// Synchronized vanilla silent flag.
501    pub silent: bool,
502    /// Server-owned vanilla glowing tag.
503    pub glowing: bool,
504    /// Vanilla scoreboard tags.
505    pub tags: Vec<String>,
506    /// Vanilla custom data compound.
507    pub custom_data_nbt: Vec<u8>,
508    /// Type-specific NBT data from `save_additional`.
509    pub nbt_data: Vec<u8>,
510    /// Direct passengers nested under this entity.
511    pub passengers: Vec<PersistentEntity>,
512}
513
514/// A scheduled tick stored with a chunk.
515///
516/// Stores the tick's position relative to the chunk, its remaining delay,
517/// priority, and the block/fluid identifier. Sub-tick order is rebuilt on load.
518#[derive(SchemaWrite, SchemaRead)]
519pub struct PersistentTick {
520    /// Relative X position within chunk (0-15).
521    pub x: u8,
522    /// Absolute Y position (world height).
523    pub y: i16,
524    /// Relative Z position within chunk (0-15).
525    pub z: u8,
526    /// Remaining delay in game ticks until this tick fires.
527    pub delay: i32,
528    /// Tick priority as `i8` (maps to `TickPriority` enum, -3 to 3).
529    pub priority: i8,
530    /// Block or fluid identifier (e.g., "`minecraft:stone_button`").
531    pub tick_type: Identifier,
532}
533
534/// A structure start stored with a chunk.
535///
536/// Only valid structure starts (those with at least one piece) are stored.
537/// Vanilla's `INVALID_START` sentinel is represented by absence from the vec.
538#[derive(SchemaWrite, SchemaRead)]
539pub struct PersistentStructureStart {
540    /// Structure type identifier (e.g., "minecraft:village").
541    pub structure: Identifier,
542    /// Origin chunk X coordinate.
543    pub chunk_x: i32,
544    /// Origin chunk Z coordinate.
545    pub chunk_z: i32,
546    /// Number of chunks referencing this structure start.
547    pub references: i32,
548    /// The pieces composing this structure.
549    pub pieces: Vec<PersistentStructurePiece>,
550}
551
552/// A structure bounding box stored as six scalar coordinates.
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SchemaWrite, SchemaRead)]
554pub struct PersistentBoundingBox {
555    /// Minimum X coordinate.
556    pub min_x: i32,
557    /// Minimum Y coordinate.
558    pub min_y: i32,
559    /// Minimum Z coordinate.
560    pub min_z: i32,
561    /// Maximum X coordinate.
562    pub max_x: i32,
563    /// Maximum Y coordinate.
564    pub max_y: i32,
565    /// Maximum Z coordinate.
566    pub max_z: i32,
567}
568
569impl PersistentBoundingBox {
570    /// Converts a runtime bounding box to its persistent representation.
571    #[must_use]
572    pub const fn from_bounding_box(bounding_box: BoundingBox) -> Self {
573        Self {
574            min_x: bounding_box.min_x(),
575            min_y: bounding_box.min_y(),
576            min_z: bounding_box.min_z(),
577            max_x: bounding_box.max_x(),
578            max_y: bounding_box.max_y(),
579            max_z: bounding_box.max_z(),
580        }
581    }
582
583    /// Converts this persistent representation to a runtime bounding box.
584    #[must_use]
585    pub const fn to_bounding_box(self) -> BoundingBox {
586        BoundingBox::new(
587            IVec3::new(self.min_x, self.min_y, self.min_z),
588            IVec3::new(self.max_x, self.max_y, self.max_z),
589        )
590    }
591}
592
593/// A structure piece stored with a chunk.
594///
595/// Common fields are stored directly; type-specific placement data is in
596/// `payload`.
597#[derive(SchemaWrite, SchemaRead)]
598pub struct PersistentStructurePiece {
599    /// Piece type identifier (e.g., "minecraft:jigsaw").
600    pub piece_type: Identifier,
601    /// Bounding box of this piece in world coordinates.
602    pub bounding_box: PersistentBoundingBox,
603    /// Generation depth in the piece tree.
604    pub gen_depth: i32,
605    /// 2D direction orientation (-1 = none, 0-3 = south/west/north/east).
606    pub orientation: i8,
607    /// Type-specific structure piece placement data.
608    pub payload: PersistentStructurePiecePayload,
609    /// Offset from piece minY to terrain ground level.
610    pub ground_level_delta: i32,
611    /// Projection mode: -1 = none, 0 = rigid, 1 = terrain matching.
612    pub projection: i8,
613    /// Jigsaw junctions used by terrain adaptation.
614    pub junctions: Vec<PersistentJigsawJunction>,
615}
616
617/// Persisted type-specific structure piece placement data.
618#[derive(SchemaWrite, SchemaRead)]
619pub enum PersistentStructurePiecePayload {
620    /// Jigsaw pool piece payload.
621    Jigsaw(PersistentJigsawPieceData),
622    /// Template-backed non-jigsaw payload.
623    Template(PersistentTemplatePieceData),
624    /// Procedural family payload.
625    Procedural(PersistentProceduralPieceData),
626}
627
628/// Steel-native persistent state for a jigsaw pool piece.
629#[derive(SchemaWrite, SchemaRead)]
630pub struct PersistentJigsawPieceData {
631    /// Selected pool element.
632    pub pool_element: PersistentPoolElement,
633    /// World-space template origin.
634    pub position: [i32; 3],
635    /// Rotation: 0=none, `1=clockwise_90`, `2=clockwise_180`, `3=counterclockwise_90`.
636    pub rotation: i8,
637    /// Liquid settings: `0=apply_waterlogging`, `1=ignore_waterlogging`.
638    pub liquid_settings: i8,
639}
640
641/// Persisted template-backed non-jigsaw piece data.
642#[derive(SchemaWrite, SchemaRead)]
643pub struct PersistentTemplatePieceData {
644    /// Structure template identifier.
645    pub template_id: Identifier,
646    /// World-space template origin.
647    pub template_position: [i32; 3],
648    /// Rotation: 0=none, `1=clockwise_90`, `2=clockwise_180`, `3=counterclockwise_90`.
649    pub rotation: i8,
650    /// Mirror: `0=none`, `1=front_back`, `2=left_right`.
651    pub mirror: i8,
652    /// Rotation pivot in template-local block coordinates.
653    pub rotation_pivot: [i32; 3],
654    /// Early block-ignore processor: `0=none`, `1=structure_block`, `2=structure_and_air`.
655    pub block_ignore: i8,
656    /// Late block-ignore processor: `0=none`, `1=structure_block`, `2=structure_and_air`.
657    pub late_block_ignore: i8,
658    /// Processors applied during block placement.
659    pub processors: PersistentTemplateProcessorList,
660    /// Liquid settings: `0=apply_waterlogging`, `1=ignore_waterlogging`.
661    pub liquid_settings: i8,
662    /// Marker handling:
663    /// `0=ignore`, `1=data_markers`, `2=shipwreck`, `3=igloo`,
664    /// `4=ocean_ruin_small`, `5=ocean_ruin_large`, `6=end_city`, `7=woodland_mansion`.
665    pub marker_handling: i8,
666    /// Family-specific position adjustment before template block placement.
667    pub placement_adjustment: PersistentTemplatePlacementAdjustment,
668    /// Placement clip: `0=center_chunk`, `1=center_chunk_expanded_to_template`,
669    /// `2=center_chunk_contains_template_center_expanded_to_template`.
670    pub placement_clip: i8,
671    /// Postprocess: `0=none`, `1=nether_fossil`, `2=igloo_top`, `3=ruined_portal`.
672    pub post_process: i8,
673}
674
675/// Persisted processors for template-backed non-jigsaw pieces.
676#[derive(SchemaWrite, SchemaRead)]
677pub enum PersistentTemplateProcessorList {
678    /// Direct empty processor list.
679    Empty,
680    /// Registry-backed processor list.
681    Registry(Identifier),
682    /// Vanilla's hardcoded ocean-ruin processor sequence.
683    OceanRuin {
684        /// Ocean ruin biome temperature: 0=warm, 1=cold.
685        biome_temp: i8,
686        /// Block-rot integrity.
687        integrity: f32,
688    },
689    /// Vanilla's hardcoded ruined-portal processor sequence.
690    RuinedPortal {
691        /// Ruined-portal vertical placement.
692        vertical_placement: i8,
693        /// Whether cold lava/aging behavior is active.
694        cold: bool,
695        /// Block age processor mossiness.
696        mossiness: f32,
697        /// Whether structure air is preserved.
698        air_pocket: bool,
699        /// Whether netherrack can grow leaves.
700        overgrown: bool,
701        /// Whether vines can be added.
702        vines: bool,
703        /// Whether blackstone replacement is active.
704        replace_with_blackstone: bool,
705    },
706}
707
708/// Persisted template position adjustment.
709#[derive(SchemaWrite, SchemaRead)]
710pub enum PersistentTemplatePlacementAdjustment {
711    /// Place at the persisted template position.
712    None,
713    /// Shipwreck height adjustment state.
714    Shipwreck {
715        /// Whether this is the beached shipwreck variant.
716        is_beached: bool,
717        /// Vanilla `height_adjusted` flag.
718        height_adjusted: bool,
719    },
720    /// Igloo per-placement height adjustment.
721    Igloo {
722        /// Vanilla template offset for this igloo piece.
723        template_offset: [i32; 3],
724    },
725    /// Ocean ruin terrain height adjustment.
726    OceanRuin,
727}
728
729/// Persisted procedural piece data.
730#[derive(SchemaWrite, SchemaRead)]
731pub enum PersistentProceduralPieceData {
732    /// Procedural family whose placement state has not been captured yet.
733    Unimplemented,
734    /// Buried treasure chest placement.
735    BuriedTreasure,
736    /// Desert pyramid piece payload.
737    DesertPyramid(PersistentDesertPyramidPieceData),
738    /// Jungle temple piece payload.
739    JungleTemple(PersistentJungleTemplePieceData),
740    /// Mineshaft room/corridor/crossing/stairs payload.
741    Mineshaft(PersistentMineshaftPieceData),
742    /// Nether fortress bridge/castle payload.
743    NetherFortress(PersistentNetherFortressPieceData),
744    /// Ocean monument building payload.
745    OceanMonument(PersistentOceanMonumentPieceData),
746    /// Stronghold recursive piece payload.
747    Stronghold(PersistentStrongholdPieceData),
748    /// Swamp hut piece payload.
749    SwampHut(PersistentSwampHutPieceData),
750}
751
752/// Persisted stronghold door variant.
753#[derive(SchemaWrite, SchemaRead)]
754pub enum PersistentStrongholdSmallDoorType {
755    /// Three-block cave-air opening.
756    Opening,
757    /// Oak door framed by stone bricks.
758    WoodDoor,
759    /// Iron-bar grate opening.
760    Grates,
761    /// Iron door with stone buttons.
762    IronDoor,
763}
764
765/// Persisted piece-specific stronghold data.
766#[derive(SchemaWrite, SchemaRead)]
767pub enum PersistentStrongholdPieceData {
768    /// Straight corridor with optional side exits.
769    Straight {
770        /// Vanilla `entryDoor`.
771        entry_door: PersistentStrongholdSmallDoorType,
772        /// Vanilla `leftChild`.
773        left_child: bool,
774        /// Vanilla `rightChild`.
775        right_child: bool,
776    },
777    /// Prison hall.
778    PrisonHall {
779        /// Vanilla `entryDoor`.
780        entry_door: PersistentStrongholdSmallDoorType,
781    },
782    /// Left turn.
783    LeftTurn {
784        /// Vanilla `entryDoor`.
785        entry_door: PersistentStrongholdSmallDoorType,
786    },
787    /// Right turn.
788    RightTurn {
789        /// Vanilla `entryDoor`.
790        entry_door: PersistentStrongholdSmallDoorType,
791    },
792    /// Room crossing with one of five vanilla decorations.
793    RoomCrossing {
794        /// Vanilla `entryDoor`.
795        entry_door: PersistentStrongholdSmallDoorType,
796        /// Vanilla `type`.
797        crossing_type: i32,
798    },
799    /// Straight stair corridor.
800    StraightStairsDown {
801        /// Vanilla `entryDoor`.
802        entry_door: PersistentStrongholdSmallDoorType,
803    },
804    /// Descending stairs, including the source/start piece.
805    StairsDown {
806        /// Vanilla `entryDoor`.
807        entry_door: PersistentStrongholdSmallDoorType,
808        /// Vanilla `isSource`.
809        is_source: bool,
810    },
811    /// Five-way crossing with low/high side exits.
812    FiveCrossing {
813        /// Vanilla `entryDoor`.
814        entry_door: PersistentStrongholdSmallDoorType,
815        /// Vanilla `leftLow`.
816        left_low: bool,
817        /// Vanilla `leftHigh`.
818        left_high: bool,
819        /// Vanilla `rightLow`.
820        right_low: bool,
821        /// Vanilla `rightHigh`.
822        right_high: bool,
823    },
824    /// Corridor containing a loot chest.
825    ChestCorridor {
826        /// Vanilla `entryDoor`.
827        entry_door: PersistentStrongholdSmallDoorType,
828        /// Vanilla `hasPlacedChest`.
829        has_placed_chest: bool,
830    },
831    /// Library room.
832    Library {
833        /// Vanilla `entryDoor`.
834        entry_door: PersistentStrongholdSmallDoorType,
835        /// Vanilla `isTall`.
836        is_tall: bool,
837    },
838    /// End portal room.
839    PortalRoom {
840        /// Vanilla `hasPlacedSpawner`.
841        has_placed_spawner: bool,
842    },
843    /// Collision filler corridor.
844    FillerCorridor {
845        /// Vanilla `steps`.
846        steps: i32,
847    },
848}
849
850/// Persisted piece-specific nether fortress data.
851#[derive(SchemaWrite, SchemaRead)]
852pub enum PersistentNetherFortressPieceData {
853    /// Bridge crossing piece.
854    BridgeCrossing,
855    /// Dead-end bridge filler piece.
856    BridgeEndFiller {
857        /// Vanilla `BridgeEndFiller.selfSeed`.
858        self_seed: i32,
859    },
860    /// Straight bridge segment.
861    BridgeStraight,
862    /// Castle corridor stair segment.
863    CastleCorridorStairs,
864    /// Castle corridor T balcony segment.
865    CastleCorridorTBalcony,
866    /// Castle entrance room.
867    CastleEntrance,
868    /// Small castle corridor crossing.
869    CastleSmallCorridorCrossing,
870    /// Small castle corridor left turn.
871    CastleSmallCorridorLeftTurn {
872        /// Vanilla `isNeedingChest`.
873        is_needing_chest: bool,
874    },
875    /// Small straight castle corridor.
876    CastleSmallCorridor,
877    /// Small castle corridor right turn.
878    CastleSmallCorridorRightTurn {
879        /// Vanilla `isNeedingChest`.
880        is_needing_chest: bool,
881    },
882    /// Nether-wart stair room.
883    CastleStalkRoom,
884    /// Blaze-spawner throne room.
885    MonsterThrone {
886        /// Vanilla `hasPlacedSpawner`.
887        has_placed_spawner: bool,
888    },
889    /// Bridge room crossing.
890    RoomCrossing,
891    /// Bridge stair room.
892    StairsRoom,
893}
894
895/// Persisted desert pyramid piece payload.
896#[derive(SchemaWrite, SchemaRead)]
897pub struct PersistentDesertPyramidPieceData {
898    /// Vanilla `ScatteredFeaturePiece.heightPosition`; -1 means not height-adjusted yet.
899    pub height_position: i32,
900    /// Chest placement flags ordered by `Direction.get2DDataValue`.
901    pub has_placed_chest: [bool; 4],
902}
903
904/// Persisted jungle temple piece payload.
905#[derive(SchemaWrite, SchemaRead)]
906pub struct PersistentJungleTemplePieceData {
907    /// Vanilla `ScatteredFeaturePiece.heightPosition`; -1 means not height-adjusted yet.
908    pub height_position: i32,
909    /// Whether the main chest has already been placed.
910    pub placed_main_chest: bool,
911    /// Whether the hidden chest has already been placed.
912    pub placed_hidden_chest: bool,
913    /// Whether the first arrow-dispenser trap has already been placed.
914    pub placed_trap1: bool,
915    /// Whether the second arrow-dispenser trap has already been placed.
916    pub placed_trap2: bool,
917}
918
919/// Persisted mineshaft piece payload.
920#[derive(SchemaWrite, SchemaRead)]
921pub struct PersistentMineshaftPieceData {
922    /// Mineshaft type: 0=normal, 1=mesa.
923    pub mineshaft_type: i8,
924    /// Piece-specific mineshaft data.
925    pub kind: PersistentMineshaftPieceKind,
926}
927
928/// Persisted piece-specific mineshaft data.
929#[derive(SchemaWrite, SchemaRead)]
930pub enum PersistentMineshaftPieceKind {
931    /// Start room.
932    Room {
933        /// Child entrance boxes.
934        child_entrance_boxes: Vec<PersistentBoundingBox>,
935    },
936    /// Horizontal corridor.
937    Corridor {
938        /// Whether rails can generate through this corridor.
939        has_rails: bool,
940        /// Whether this is a cobweb-heavy cave-spider corridor.
941        spider_corridor: bool,
942        /// Whether the cave-spider spawner has already been placed.
943        has_placed_spider: bool,
944        /// Number of five-block corridor sections.
945        num_sections: i32,
946    },
947    /// Corridor crossing.
948    Crossing {
949        /// Direction: 0=south, 1=west, 2=north, 3=east.
950        direction: i8,
951        /// Whether the crossing has the upper floor.
952        is_two_floored: bool,
953    },
954    /// Stair segment.
955    Stairs,
956}
957
958/// Persisted swamp hut piece payload.
959#[derive(SchemaWrite, SchemaRead)]
960pub struct PersistentSwampHutPieceData {
961    /// Vanilla `ScatteredFeaturePiece.heightPosition`; -1 means not height-adjusted yet.
962    pub height_position: i32,
963    /// Whether the structure witch has already been spawned.
964    pub spawned_witch: bool,
965    /// Whether the structure black cat has already been spawned.
966    pub spawned_cat: bool,
967}
968
969/// Persisted ocean monument building payload.
970#[derive(SchemaWrite, SchemaRead)]
971pub struct PersistentOceanMonumentPieceData {
972    /// Internal child pieces generated by vanilla `MonumentBuilding`.
973    pub child_pieces: Vec<PersistentOceanMonumentChildPiece>,
974}
975
976/// Persisted internal ocean monument child piece.
977#[derive(SchemaWrite, SchemaRead)]
978pub struct PersistentOceanMonumentChildPiece {
979    /// World-space child bounding box after building-relative offset.
980    pub bounding_box: PersistentBoundingBox,
981    /// Child piece variant and variant-specific placement state.
982    pub kind: PersistentOceanMonumentChildPieceKind,
983}
984
985/// Persisted ocean monument child piece variant.
986#[derive(SchemaWrite, SchemaRead)]
987pub enum PersistentOceanMonumentChildPieceKind {
988    /// `OceanMonumentEntryRoom`.
989    EntryRoom {
990        /// Source room snapshot.
991        room: PersistentOceanMonumentRoomData,
992    },
993    /// `OceanMonumentCoreRoom`.
994    CoreRoom,
995    /// `OceanMonumentDoubleXRoom`.
996    DoubleXRoom {
997        /// Western room snapshot.
998        west: PersistentOceanMonumentRoomData,
999        /// Eastern room snapshot.
1000        east: PersistentOceanMonumentRoomData,
1001    },
1002    /// `OceanMonumentDoubleXYRoom`.
1003    DoubleXYRoom {
1004        /// Lower western room.
1005        west: PersistentOceanMonumentRoomData,
1006        /// Lower eastern room.
1007        east: PersistentOceanMonumentRoomData,
1008        /// Upper western room.
1009        west_up: PersistentOceanMonumentRoomData,
1010        /// Upper eastern room.
1011        east_up: PersistentOceanMonumentRoomData,
1012    },
1013    /// `OceanMonumentDoubleYRoom`.
1014    DoubleYRoom {
1015        /// Lower room.
1016        room: PersistentOceanMonumentRoomData,
1017        /// Upper room.
1018        above: PersistentOceanMonumentRoomData,
1019    },
1020    /// `OceanMonumentDoubleYZRoom`.
1021    DoubleYZRoom {
1022        /// Southern lower room.
1023        south: PersistentOceanMonumentRoomData,
1024        /// Northern lower room.
1025        north: PersistentOceanMonumentRoomData,
1026        /// Southern upper room.
1027        south_up: PersistentOceanMonumentRoomData,
1028        /// Northern upper room.
1029        north_up: PersistentOceanMonumentRoomData,
1030    },
1031    /// `OceanMonumentDoubleZRoom`.
1032    DoubleZRoom {
1033        /// Southern room.
1034        south: PersistentOceanMonumentRoomData,
1035        /// Northern room.
1036        north: PersistentOceanMonumentRoomData,
1037    },
1038    /// `OceanMonumentSimpleRoom`.
1039    SimpleRoom {
1040        /// Room snapshot.
1041        room: PersistentOceanMonumentRoomData,
1042        /// Vanilla `mainDesign`.
1043        main_design: i32,
1044    },
1045    /// `OceanMonumentSimpleTopRoom`.
1046    SimpleTopRoom {
1047        /// Room snapshot.
1048        room: PersistentOceanMonumentRoomData,
1049    },
1050    /// `OceanMonumentWingRoom`.
1051    WingRoom {
1052        /// Vanilla `mainDesign`.
1053        main_design: i32,
1054    },
1055    /// `OceanMonumentPenthouse`.
1056    Penthouse,
1057}
1058
1059/// Persisted ocean monument room snapshot.
1060#[derive(SchemaWrite, SchemaRead)]
1061pub struct PersistentOceanMonumentRoomData {
1062    /// Vanilla room index.
1063    pub index: i32,
1064    /// Vanilla `hasOpening`, ordered by `Direction.get3DDataValue`.
1065    pub has_opening: [bool; 6],
1066    /// Whether `connections[UP] != null`.
1067    pub has_up_connection: bool,
1068}
1069
1070/// Persisted pool element selected during jigsaw assembly.
1071#[derive(SchemaWrite, SchemaRead)]
1072pub enum PersistentPoolElement {
1073    /// Single structure template piece.
1074    Single {
1075        /// Template location.
1076        location: Identifier,
1077        /// Processors applied during block placement.
1078        processors: PersistentProcessorList,
1079        /// Projection mode: 0 = rigid, 1 = terrain matching.
1080        projection: i8,
1081    },
1082    /// Legacy single piece.
1083    LegacySingle {
1084        /// Template location.
1085        location: Identifier,
1086        /// Processors applied during block placement.
1087        processors: PersistentProcessorList,
1088        /// Projection mode: 0 = rigid, 1 = terrain matching.
1089        projection: i8,
1090    },
1091    /// Empty placeholder element.
1092    Empty,
1093    /// Placed feature element.
1094    Feature {
1095        /// Feature identifier.
1096        feature: Identifier,
1097        /// Projection mode: 0 = rigid, 1 = terrain matching.
1098        projection: i8,
1099    },
1100    /// Group of sub-elements.
1101    List {
1102        /// Sub-elements.
1103        elements: Vec<PersistentPoolElement>,
1104        /// Projection mode: 0 = rigid, 1 = terrain matching.
1105        projection: i8,
1106    },
1107}
1108
1109/// Persisted processor list holder for single pool elements.
1110#[derive(SchemaWrite, SchemaRead)]
1111pub enum PersistentProcessorList {
1112    /// Direct empty processor list.
1113    Empty,
1114    /// Registry-backed processor list.
1115    Registry(Identifier),
1116}
1117
1118/// A persisted jigsaw junction used by Beardifier terrain adaptation.
1119#[derive(SchemaWrite, SchemaRead)]
1120pub struct PersistentJigsawJunction {
1121    /// World X.
1122    pub source_x: i32,
1123    /// Ground-adjusted Y.
1124    pub source_ground_y: i32,
1125    /// World Z.
1126    pub source_z: i32,
1127    /// Y delta between source and target.
1128    pub delta_y: i32,
1129    /// Destination projection: 0 = rigid, 1 = terrain matching.
1130    pub dest_projection: i8,
1131}
1132
1133/// A structure reference entry stored with a chunk.
1134///
1135/// References point to structure starts in nearby origin chunks.
1136#[derive(SchemaWrite, SchemaRead)]
1137pub struct PersistentStructureReference {
1138    /// Structure type identifier.
1139    pub structure: Identifier,
1140    /// Packed chunk positions of origin chunks.
1141    pub references: Vec<PackedChunkPos>,
1142}
1143
1144/// A point of interest's occupancy state stored with a chunk.
1145///
1146/// Only the position and remaining free tickets are persisted — the POI type
1147/// is derived from the block state on load via `scan_and_populate`.
1148#[derive(SchemaWrite, SchemaRead)]
1149pub struct PersistentPoi {
1150    /// Relative X position within chunk (0-15).
1151    pub x: u8,
1152    /// Absolute Y position (world height).
1153    pub y: i16,
1154    /// Relative Z position within chunk (0-15).
1155    pub z: u8,
1156    /// Number of tickets still available for claiming.
1157    pub free_tickets: u32,
1158}
1159
1160/// Position of a region in region coordinates.
1161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1162pub struct RegionPos {
1163    /// Region X coordinate (`chunk_x` / 32).
1164    pub x: i32,
1165    /// Region Z coordinate (`chunk_z` / 32).
1166    pub z: i32,
1167}
1168
1169impl RegionPos {
1170    /// Creates a new region position.
1171    #[must_use]
1172    pub const fn new(x: i32, z: i32) -> Self {
1173        Self { x, z }
1174    }
1175
1176    /// Converts a chunk position to a region position.
1177    #[must_use]
1178    pub const fn from_chunk(chunk_x: i32, chunk_z: i32) -> Self {
1179        Self {
1180            x: chunk_x.div_euclid(REGION_SIZE as i32),
1181            z: chunk_z.div_euclid(REGION_SIZE as i32),
1182        }
1183    }
1184
1185    /// Gets the local chunk coordinates within this region for a global chunk position.
1186    #[must_use]
1187    pub const fn local_chunk_pos(chunk_x: i32, chunk_z: i32) -> (usize, usize) {
1188        (
1189            chunk_x.rem_euclid(REGION_SIZE as i32) as usize,
1190            chunk_z.rem_euclid(REGION_SIZE as i32) as usize,
1191        )
1192    }
1193
1194    /// Returns the filename for this region (e.g., "r.0.-1.srg").
1195    #[must_use]
1196    pub fn filename(self) -> String {
1197        format!("r.{}.{}.srg", self.x, self.z)
1198    }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204
1205    #[test]
1206    fn persistent_block_state_properties_round_trip() {
1207        let state = PersistentBlockState {
1208            name: Identifier::vanilla_static("oak_stairs"),
1209            properties: vec![("facing", "north"), ("waterlogged", "false")],
1210        };
1211
1212        let encoded = wincode::serialize(&state).expect("block state should serialize");
1213        let decoded: PersistentBlockState<'_> =
1214            wincode::deserialize_exact(&encoded).expect("block state should deserialize");
1215
1216        assert_eq!(decoded, state);
1217    }
1218
1219    #[test]
1220    fn test_region_pos_from_chunk() {
1221        // Positive chunks
1222        assert_eq!(RegionPos::from_chunk(0, 0), RegionPos::new(0, 0));
1223        assert_eq!(RegionPos::from_chunk(31, 31), RegionPos::new(0, 0));
1224        assert_eq!(RegionPos::from_chunk(32, 32), RegionPos::new(1, 1));
1225
1226        // Negative chunks
1227        assert_eq!(RegionPos::from_chunk(-1, -1), RegionPos::new(-1, -1));
1228        assert_eq!(RegionPos::from_chunk(-32, -32), RegionPos::new(-1, -1));
1229        assert_eq!(RegionPos::from_chunk(-33, -33), RegionPos::new(-2, -2));
1230    }
1231
1232    #[test]
1233    fn test_local_chunk_pos() {
1234        assert_eq!(RegionPos::local_chunk_pos(0, 0), (0, 0));
1235        assert_eq!(RegionPos::local_chunk_pos(31, 31), (31, 31));
1236        assert_eq!(RegionPos::local_chunk_pos(32, 32), (0, 0));
1237        assert_eq!(RegionPos::local_chunk_pos(-1, -1), (31, 31));
1238        assert_eq!(RegionPos::local_chunk_pos(-32, -32), (0, 0));
1239    }
1240
1241    #[test]
1242    fn test_chunk_index() {
1243        assert_eq!(RegionHeader::chunk_index(0, 0), 0);
1244        assert_eq!(RegionHeader::chunk_index(31, 0), 31);
1245        assert_eq!(RegionHeader::chunk_index(0, 1), 32);
1246        assert_eq!(RegionHeader::chunk_index(31, 31), 1023);
1247    }
1248
1249    #[test]
1250    fn test_chunk_entry_roundtrip() {
1251        let entry = ChunkEntry::new(42, 12345, ChunkStatus::Full);
1252        let bytes = entry.to_bytes();
1253        let decoded = ChunkEntry::from_bytes(bytes).expect("serialized entry should decode");
1254        assert_eq!(entry.sector_offset, decoded.sector_offset);
1255        assert_eq!(entry.size_bytes, decoded.size_bytes);
1256        assert_eq!(entry.status, decoded.status);
1257    }
1258
1259    #[test]
1260    fn test_chunk_entry_empty() {
1261        let entry = ChunkEntry::default();
1262        assert!(!entry.exists());
1263        assert_eq!(entry.sector_count(), 0);
1264    }
1265
1266    #[test]
1267    fn test_sector_count() {
1268        // Empty
1269        assert_eq!(ChunkEntry::new(1, 0, ChunkStatus::Full).sector_count(), 0);
1270        // Exactly one sector
1271        assert_eq!(
1272            ChunkEntry::new(1, 4096, ChunkStatus::Full).sector_count(),
1273            1
1274        );
1275        // Just over one sector
1276        assert_eq!(
1277            ChunkEntry::new(1, 4097, ChunkStatus::Full).sector_count(),
1278            2
1279        );
1280        // Multiple sectors
1281        assert_eq!(
1282            ChunkEntry::new(1, 12000, ChunkStatus::Full).sector_count(),
1283            3
1284        );
1285    }
1286
1287    #[test]
1288    fn test_find_free_sectors_empty() {
1289        let header = RegionHeader::new();
1290        // Should return first data sector
1291        assert_eq!(header.find_free_sectors(1, 3), FIRST_DATA_SECTOR);
1292    }
1293
1294    #[test]
1295    fn test_find_free_sectors_gap() {
1296        let mut header = RegionHeader::new();
1297        // Chunk at sector 3-4 (2 sectors)
1298        header.entries[0] = ChunkEntry::new(3, 8000, ChunkStatus::Full);
1299        // Chunk at sector 10-11 (2 sectors)
1300        header.entries[1] = ChunkEntry::new(10, 8000, ChunkStatus::Full);
1301
1302        // Should find gap at sector 5-9
1303        assert_eq!(header.find_free_sectors(3, 12), 5);
1304        // Needs more than gap, append at end
1305        assert_eq!(header.find_free_sectors(6, 12), 12);
1306    }
1307}