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}