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