Skip to main content

sectorsync_core/
component.rs

1//! Custom component registry and station-local blob storage.
2
3use std::collections::HashMap;
4
5use crate::ids::{ComponentId, EntityHandle};
6use crate::spatial::Vec3;
7
8/// Component codec error used by built-in codecs.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ComponentCodecError {
11    /// Input length did not match codec expectation.
12    ExpectedBytes {
13        /// Expected byte count.
14        expected: usize,
15        /// Actual byte count.
16        actual: usize,
17    },
18}
19
20impl core::fmt::Display for ComponentCodecError {
21    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22        match self {
23            Self::ExpectedBytes { expected, actual } => {
24                write!(f, "expected {expected} bytes, got {actual}")
25            }
26        }
27    }
28}
29
30impl std::error::Error for ComponentCodecError {}
31
32/// Typed component codec. Embedders can implement this for their own compact
33/// schema and bit-packing formats.
34pub trait ComponentCodec<T> {
35    /// Encodes `value` into `out`.
36    fn encode(&self, value: &T, out: &mut Vec<u8>) -> Result<(), ComponentCodecError>;
37
38    /// Decodes a value from bytes.
39    fn decode(&self, input: &[u8]) -> Result<T, ComponentCodecError>;
40
41    /// Fixed encoded size when known.
42    fn fixed_size(&self) -> Option<usize> {
43        None
44    }
45}
46
47/// Little-endian `u32` codec.
48#[derive(Clone, Copy, Debug, Default)]
49pub struct U32LeCodec;
50
51impl ComponentCodec<u32> for U32LeCodec {
52    fn encode(&self, value: &u32, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
53        out.extend_from_slice(&value.to_le_bytes());
54        Ok(())
55    }
56
57    fn decode(&self, input: &[u8]) -> Result<u32, ComponentCodecError> {
58        let bytes = exact_array::<4>(input)?;
59        Ok(u32::from_le_bytes(bytes))
60    }
61
62    fn fixed_size(&self) -> Option<usize> {
63        Some(4)
64    }
65}
66
67/// Little-endian `f32` codec.
68#[derive(Clone, Copy, Debug, Default)]
69pub struct F32LeCodec;
70
71impl ComponentCodec<f32> for F32LeCodec {
72    fn encode(&self, value: &f32, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
73        out.extend_from_slice(&value.to_le_bytes());
74        Ok(())
75    }
76
77    fn decode(&self, input: &[u8]) -> Result<f32, ComponentCodecError> {
78        let bytes = exact_array::<4>(input)?;
79        Ok(f32::from_le_bytes(bytes))
80    }
81
82    fn fixed_size(&self) -> Option<usize> {
83        Some(4)
84    }
85}
86
87/// Little-endian `Vec3` codec.
88#[derive(Clone, Copy, Debug, Default)]
89pub struct Vec3LeCodec;
90
91impl ComponentCodec<Vec3> for Vec3LeCodec {
92    fn encode(&self, value: &Vec3, out: &mut Vec<u8>) -> Result<(), ComponentCodecError> {
93        out.extend_from_slice(&value.x.to_le_bytes());
94        out.extend_from_slice(&value.y.to_le_bytes());
95        out.extend_from_slice(&value.z.to_le_bytes());
96        Ok(())
97    }
98
99    fn decode(&self, input: &[u8]) -> Result<Vec3, ComponentCodecError> {
100        if input.len() != 12 {
101            return Err(ComponentCodecError::ExpectedBytes {
102                expected: 12,
103                actual: input.len(),
104            });
105        }
106        let x = f32::from_le_bytes(input[0..4].try_into().expect("slice length checked"));
107        let y = f32::from_le_bytes(input[4..8].try_into().expect("slice length checked"));
108        let z = f32::from_le_bytes(input[8..12].try_into().expect("slice length checked"));
109        Ok(Vec3 { x, y, z })
110    }
111
112    fn fixed_size(&self) -> Option<usize> {
113        Some(12)
114    }
115}
116
117fn exact_array<const N: usize>(input: &[u8]) -> Result<[u8; N], ComponentCodecError> {
118    if input.len() != N {
119        return Err(ComponentCodecError::ExpectedBytes {
120            expected: N,
121            actual: input.len(),
122        });
123    }
124    let mut out = [0_u8; N];
125    out.copy_from_slice(input);
126    Ok(out)
127}
128
129/// Storage strategy declared by a registered component.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum ComponentStorageKind {
132    /// `SectorSync` stores opaque component bytes in a sparse station-local column.
133    SparseBlob,
134    /// Component data lives outside `SectorSync`; the registry only documents it.
135    External,
136}
137
138/// Synchronization behavior declared by a registered component.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum ComponentSyncMode {
141    /// Component is never replicated by `SectorSync`.
142    NotReplicated,
143    /// Component is replicated as delta when dirty.
144    Delta,
145    /// Component is sent as a snapshot when selected by policy.
146    Snapshot,
147    /// Component changes are represented by events.
148    EventOnly,
149}
150
151/// Migration behavior declared by a registered component.
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum ComponentMigrationMode {
154    /// Copy component bytes during owner handoff.
155    Copy,
156    /// Drop component bytes during owner handoff.
157    Drop,
158    /// External system owns migration.
159    External,
160}
161
162/// Registered component descriptor.
163#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct ComponentDescriptor {
165    /// Component id used in hot-path records.
166    pub id: ComponentId,
167    /// Stable debug name.
168    pub name: &'static str,
169    /// Storage strategy.
170    pub storage: ComponentStorageKind,
171    /// Synchronization strategy.
172    pub sync: ComponentSyncMode,
173    /// Migration strategy.
174    pub migration: ComponentMigrationMode,
175    /// Maximum accepted blob size in bytes for `SectorSync`-owned storage.
176    pub max_bytes: usize,
177    /// Stable schema hash selected by the embedding application.
178    pub schema_hash: u64,
179}
180
181impl ComponentDescriptor {
182    /// Creates a sparse blob descriptor.
183    pub const fn sparse_blob(
184        id: ComponentId,
185        name: &'static str,
186        sync: ComponentSyncMode,
187        migration: ComponentMigrationMode,
188        max_bytes: usize,
189    ) -> Self {
190        Self {
191            id,
192            name,
193            storage: ComponentStorageKind::SparseBlob,
194            sync,
195            migration,
196            max_bytes,
197            schema_hash: 0,
198        }
199    }
200
201    /// Attaches a stable schema hash to this descriptor.
202    #[must_use]
203    pub const fn with_schema_hash(mut self, schema_hash: u64) -> Self {
204        self.schema_hash = schema_hash;
205        self
206    }
207}
208
209/// Typed component schema descriptor.
210#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct ComponentSchema {
212    /// Component descriptor.
213    pub descriptor: ComponentDescriptor,
214    /// Fixed encoded size when known.
215    pub fixed_size: Option<usize>,
216}
217
218impl ComponentSchema {
219    /// Creates a typed schema from a descriptor and codec.
220    pub fn new<T, C: ComponentCodec<T>>(descriptor: ComponentDescriptor, codec: &C) -> Self {
221        Self {
222            descriptor,
223            fixed_size: codec.fixed_size(),
224        }
225    }
226}
227
228/// Fixed field type used by generated component schema helpers.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum ComponentFieldType {
231    /// Unsigned 8-bit integer.
232    U8,
233    /// Unsigned 16-bit little-endian integer.
234    U16,
235    /// Unsigned 32-bit little-endian integer.
236    U32,
237    /// Unsigned 64-bit little-endian integer.
238    U64,
239    /// Signed 32-bit little-endian integer.
240    I32,
241    /// 32-bit little-endian floating point value.
242    F32,
243    /// Three little-endian `f32` values.
244    Vec3,
245    /// Opaque bytes with a maximum generated layout size.
246    Bytes {
247        /// Maximum byte count reserved in the generated layout.
248        max_len: usize,
249    },
250}
251
252impl ComponentFieldType {
253    /// Maximum encoded size in bytes.
254    pub const fn max_size(self) -> usize {
255        match self {
256            Self::U8 => 1,
257            Self::U16 => 2,
258            Self::U32 | Self::I32 | Self::F32 => 4,
259            Self::U64 => 8,
260            Self::Vec3 => 12,
261            Self::Bytes { max_len } => max_len,
262        }
263    }
264
265    const fn tag(self) -> u8 {
266        match self {
267            Self::U8 => 1,
268            Self::U16 => 2,
269            Self::U32 => 3,
270            Self::U64 => 4,
271            Self::I32 => 5,
272            Self::F32 => 6,
273            Self::Vec3 => 7,
274            Self::Bytes { .. } => 8,
275        }
276    }
277}
278
279/// Field descriptor emitted by external schema generators.
280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub struct ComponentFieldDescriptor {
282    /// Stable field name.
283    pub name: &'static str,
284    /// Encoded field type.
285    pub ty: ComponentFieldType,
286    /// Byte offset inside the generated component blob.
287    pub offset: usize,
288}
289
290impl ComponentFieldDescriptor {
291    /// Creates a generated field descriptor.
292    pub const fn new(name: &'static str, ty: ComponentFieldType, offset: usize) -> Self {
293        Self { name, ty, offset }
294    }
295
296    /// Returns the exclusive end offset.
297    pub const fn end_offset(self) -> usize {
298        self.offset.saturating_add(self.ty.max_size())
299    }
300}
301
302/// Component schema shape emitted by an external generator.
303#[derive(Clone, Copy, Debug, PartialEq, Eq)]
304pub struct GeneratedComponentSchema {
305    /// Component id used in hot-path records.
306    pub id: ComponentId,
307    /// Stable debug name.
308    pub name: &'static str,
309    /// Storage strategy.
310    pub storage: ComponentStorageKind,
311    /// Synchronization strategy.
312    pub sync: ComponentSyncMode,
313    /// Migration strategy.
314    pub migration: ComponentMigrationMode,
315    /// Maximum accepted blob size in bytes.
316    pub max_bytes: usize,
317    /// Generated field layout.
318    pub fields: &'static [ComponentFieldDescriptor],
319}
320
321impl GeneratedComponentSchema {
322    /// Creates a generated component schema.
323    pub const fn new(
324        id: ComponentId,
325        name: &'static str,
326        storage: ComponentStorageKind,
327        sync: ComponentSyncMode,
328        migration: ComponentMigrationMode,
329        max_bytes: usize,
330        fields: &'static [ComponentFieldDescriptor],
331    ) -> Self {
332        Self {
333            id,
334            name,
335            storage,
336            sync,
337            migration,
338            max_bytes,
339            fields,
340        }
341    }
342
343    /// Validates generated layout invariants.
344    pub fn validate(&self) -> Result<(), ComponentSchemaError> {
345        for (index, field) in self.fields.iter().enumerate() {
346            if field.end_offset() > self.max_bytes {
347                return Err(ComponentSchemaError::FieldOutOfBounds {
348                    name: field.name,
349                    offset: field.offset,
350                    size: field.ty.max_size(),
351                    max_bytes: self.max_bytes,
352                });
353            }
354
355            for earlier in &self.fields[..index] {
356                if earlier.name == field.name {
357                    return Err(ComponentSchemaError::DuplicateFieldName(field.name));
358                }
359                if ranges_overlap(
360                    earlier.offset,
361                    earlier.end_offset(),
362                    field.offset,
363                    field.end_offset(),
364                ) {
365                    return Err(ComponentSchemaError::FieldOverlap {
366                        left: earlier.name,
367                        right: field.name,
368                    });
369                }
370            }
371        }
372        Ok(())
373    }
374
375    /// Returns the generated schema hash.
376    pub fn schema_hash(&self) -> u64 {
377        let mut hash = FNV_OFFSET;
378        hash = hash_u64(hash, self.id.get().into());
379        hash = hash_str(hash, self.name);
380        hash = hash_u8(hash, storage_tag(self.storage));
381        hash = hash_u8(hash, sync_tag(self.sync));
382        hash = hash_u8(hash, migration_tag(self.migration));
383        hash = hash_u64(hash, self.max_bytes as u64);
384        for field in self.fields {
385            hash = hash_str(hash, field.name);
386            hash = hash_u8(hash, field.ty.tag());
387            hash = hash_u64(hash, field.ty.max_size() as u64);
388            hash = hash_u64(hash, field.offset as u64);
389        }
390        hash
391    }
392
393    /// Returns the maximum generated fixed size.
394    pub fn fixed_size(&self) -> Option<usize> {
395        self.fields
396            .iter()
397            .map(|field| field.end_offset())
398            .max()
399            .or(Some(0))
400    }
401
402    /// Builds a component descriptor with the generated schema hash attached.
403    pub fn descriptor(&self) -> ComponentDescriptor {
404        ComponentDescriptor {
405            id: self.id,
406            name: self.name,
407            storage: self.storage,
408            sync: self.sync,
409            migration: self.migration,
410            max_bytes: self.max_bytes,
411            schema_hash: self.schema_hash(),
412        }
413    }
414
415    /// Builds a typed component schema wrapper.
416    pub fn component_schema(&self) -> ComponentSchema {
417        ComponentSchema {
418            descriptor: self.descriptor(),
419            fixed_size: self.fixed_size(),
420        }
421    }
422}
423
424/// Generated component schema validation error.
425#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426pub enum ComponentSchemaError {
427    /// Field name was repeated.
428    DuplicateFieldName(&'static str),
429    /// Field range exceeded `max_bytes`.
430    FieldOutOfBounds {
431        /// Field name.
432        name: &'static str,
433        /// Field offset.
434        offset: usize,
435        /// Field size.
436        size: usize,
437        /// Maximum component bytes.
438        max_bytes: usize,
439    },
440    /// Two field byte ranges overlap.
441    FieldOverlap {
442        /// Earlier field.
443        left: &'static str,
444        /// Later field.
445        right: &'static str,
446    },
447}
448
449impl core::fmt::Display for ComponentSchemaError {
450    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
451        match self {
452            Self::DuplicateFieldName(name) => write!(f, "duplicate component field {name}"),
453            Self::FieldOutOfBounds {
454                name,
455                offset,
456                size,
457                max_bytes,
458            } => write!(
459                f,
460                "component field {name} at {offset} with size {size} exceeds max bytes {max_bytes}"
461            ),
462            Self::FieldOverlap { left, right } => {
463                write!(f, "component fields {left} and {right} overlap")
464            }
465        }
466    }
467}
468
469impl std::error::Error for ComponentSchemaError {}
470
471/// Component registry error.
472#[derive(Clone, Debug, PartialEq, Eq)]
473pub enum ComponentRegistryError {
474    /// Component id is already registered.
475    DuplicateId(ComponentId),
476    /// Component name is already registered.
477    DuplicateName(&'static str),
478}
479
480impl core::fmt::Display for ComponentRegistryError {
481    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
482        match self {
483            Self::DuplicateId(id) => write!(f, "duplicate component id {}", id.get()),
484            Self::DuplicateName(name) => write!(f, "duplicate component name {name}"),
485        }
486    }
487}
488
489impl std::error::Error for ComponentRegistryError {}
490
491/// Error produced while registering a generated schema.
492#[derive(Clone, Debug, PartialEq, Eq)]
493pub enum GeneratedSchemaRegistrationError {
494    /// Generated schema validation failed.
495    Schema(ComponentSchemaError),
496    /// Component registry rejected the generated descriptor.
497    Registry(ComponentRegistryError),
498}
499
500impl core::fmt::Display for GeneratedSchemaRegistrationError {
501    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
502        match self {
503            Self::Schema(error) => write!(f, "{error}"),
504            Self::Registry(error) => write!(f, "{error}"),
505        }
506    }
507}
508
509impl std::error::Error for GeneratedSchemaRegistrationError {
510    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
511        match self {
512            Self::Schema(error) => Some(error),
513            Self::Registry(error) => Some(error),
514        }
515    }
516}
517
518/// Dense component descriptor registry.
519#[derive(Clone, Debug, Default)]
520pub struct ComponentRegistry {
521    descriptors: Vec<Option<ComponentDescriptor>>,
522}
523
524impl ComponentRegistry {
525    /// Registers a component descriptor.
526    pub fn register(
527        &mut self,
528        descriptor: ComponentDescriptor,
529    ) -> Result<(), ComponentRegistryError> {
530        if self.get(descriptor.id).is_some() {
531            return Err(ComponentRegistryError::DuplicateId(descriptor.id));
532        }
533        if self.iter().any(|existing| existing.name == descriptor.name) {
534            return Err(ComponentRegistryError::DuplicateName(descriptor.name));
535        }
536
537        let index = usize::from(descriptor.id.get());
538        if self.descriptors.len() <= index {
539            self.descriptors.resize(index + 1, None);
540        }
541        self.descriptors[index] = Some(descriptor);
542        Ok(())
543    }
544
545    /// Validates and registers a generated component schema.
546    pub fn register_generated_schema(
547        &mut self,
548        schema: &GeneratedComponentSchema,
549    ) -> Result<ComponentSchema, GeneratedSchemaRegistrationError> {
550        schema
551            .validate()
552            .map_err(GeneratedSchemaRegistrationError::Schema)?;
553        let component_schema = schema.component_schema();
554        self.register(component_schema.descriptor.clone())
555            .map_err(GeneratedSchemaRegistrationError::Registry)?;
556        Ok(component_schema)
557    }
558
559    /// Gets a descriptor by component id.
560    pub fn get(&self, id: ComponentId) -> Option<&ComponentDescriptor> {
561        self.descriptors
562            .get(usize::from(id.get()))
563            .and_then(Option::as_ref)
564    }
565
566    /// Iterates over descriptors.
567    pub fn iter(&self) -> impl Iterator<Item = &ComponentDescriptor> {
568        self.descriptors.iter().filter_map(Option::as_ref)
569    }
570
571    /// Number of registered descriptors.
572    pub fn len(&self) -> usize {
573        self.iter().count()
574    }
575
576    /// Returns whether no descriptors are registered.
577    pub fn is_empty(&self) -> bool {
578        self.len() == 0
579    }
580}
581
582const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
583const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
584
585fn hash_u8(hash: u64, value: u8) -> u64 {
586    (hash ^ u64::from(value)).wrapping_mul(FNV_PRIME)
587}
588
589fn hash_u64(mut hash: u64, value: u64) -> u64 {
590    for byte in value.to_le_bytes() {
591        hash = hash_u8(hash, byte);
592    }
593    hash
594}
595
596fn hash_str(mut hash: u64, value: &str) -> u64 {
597    for byte in value.bytes() {
598        hash = hash_u8(hash, byte);
599    }
600    hash_u8(hash, 0)
601}
602
603fn storage_tag(storage: ComponentStorageKind) -> u8 {
604    match storage {
605        ComponentStorageKind::SparseBlob => 1,
606        ComponentStorageKind::External => 2,
607    }
608}
609
610fn sync_tag(sync: ComponentSyncMode) -> u8 {
611    match sync {
612        ComponentSyncMode::NotReplicated => 0,
613        ComponentSyncMode::Delta => 1,
614        ComponentSyncMode::Snapshot => 2,
615        ComponentSyncMode::EventOnly => 3,
616    }
617}
618
619fn migration_tag(migration: ComponentMigrationMode) -> u8 {
620    match migration {
621        ComponentMigrationMode::Copy => 1,
622        ComponentMigrationMode::Drop => 2,
623        ComponentMigrationMode::External => 3,
624    }
625}
626
627fn ranges_overlap(
628    left_start: usize,
629    left_end: usize,
630    right_start: usize,
631    right_end: usize,
632) -> bool {
633    left_start < right_end && right_start < left_end
634}
635
636/// Opaque component blob stored in a station-local component column.
637#[derive(Clone, Debug, Default, PartialEq, Eq)]
638pub struct ComponentBlob {
639    /// Monotonic version selected by the writer.
640    pub version: u64,
641    /// Dirty flag used by replication planners.
642    pub dirty: bool,
643    /// Opaque bytes.
644    pub bytes: Vec<u8>,
645}
646
647/// Component storage error.
648#[derive(Clone, Copy, Debug, PartialEq, Eq)]
649pub enum ComponentStoreError {
650    /// Descriptor does not use `SectorSync`-owned blob storage.
651    NotBlobStorage(ComponentId),
652    /// Blob exceeds descriptor limit.
653    BlobTooLarge {
654        /// Component id.
655        component_id: ComponentId,
656        /// Blob size in bytes.
657        actual: usize,
658        /// Maximum allowed size in bytes.
659        max: usize,
660    },
661    /// Codec failed while encoding or decoding.
662    Codec(ComponentCodecError),
663    /// Component blob does not exist.
664    MissingBlob(ComponentId),
665}
666
667impl core::fmt::Display for ComponentStoreError {
668    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
669        match self {
670            Self::NotBlobStorage(id) => {
671                write!(f, "component {} is not SectorSync blob storage", id.get())
672            }
673            Self::BlobTooLarge {
674                component_id,
675                actual,
676                max,
677            } => write!(
678                f,
679                "component {} blob has {} bytes, max {}",
680                component_id.get(),
681                actual,
682                max
683            ),
684            Self::Codec(error) => write!(f, "{error}"),
685            Self::MissingBlob(id) => write!(f, "component {} blob is missing", id.get()),
686        }
687    }
688}
689
690impl std::error::Error for ComponentStoreError {}
691
692#[derive(Clone, Debug, Default)]
693struct ComponentColumn {
694    values: HashMap<EntityHandle, ComponentBlob>,
695}
696
697/// Station-local sparse component blob store.
698#[derive(Clone, Debug, Default)]
699pub struct ComponentStore {
700    columns: Vec<Option<ComponentColumn>>,
701}
702
703impl ComponentStore {
704    /// Writes an opaque component blob.
705    pub fn set_blob(
706        &mut self,
707        descriptor: &ComponentDescriptor,
708        entity: EntityHandle,
709        version: u64,
710        bytes: Vec<u8>,
711    ) -> Result<(), ComponentStoreError> {
712        if descriptor.storage != ComponentStorageKind::SparseBlob {
713            return Err(ComponentStoreError::NotBlobStorage(descriptor.id));
714        }
715        if bytes.len() > descriptor.max_bytes {
716            return Err(ComponentStoreError::BlobTooLarge {
717                component_id: descriptor.id,
718                actual: bytes.len(),
719                max: descriptor.max_bytes,
720            });
721        }
722
723        let column = self.column_mut(descriptor.id);
724        column.values.insert(
725            entity,
726            ComponentBlob {
727                version,
728                dirty: true,
729                bytes,
730            },
731        );
732        Ok(())
733    }
734
735    /// Encodes and writes a typed component value using `codec`.
736    pub fn set_typed<T, C: ComponentCodec<T>>(
737        &mut self,
738        descriptor: &ComponentDescriptor,
739        entity: EntityHandle,
740        version: u64,
741        codec: &C,
742        value: &T,
743    ) -> Result<(), ComponentStoreError> {
744        let mut bytes = Vec::with_capacity(codec.fixed_size().unwrap_or(0));
745        codec
746            .encode(value, &mut bytes)
747            .map_err(ComponentStoreError::Codec)?;
748        self.set_blob(descriptor, entity, version, bytes)
749    }
750
751    /// Gets an opaque component blob.
752    pub fn get_blob(
753        &self,
754        component_id: ComponentId,
755        entity: EntityHandle,
756    ) -> Option<&ComponentBlob> {
757        self.columns
758            .get(usize::from(component_id.get()))
759            .and_then(Option::as_ref)
760            .and_then(|column| column.values.get(&entity))
761    }
762
763    /// Decodes a typed component value using `codec`.
764    pub fn get_typed<T, C: ComponentCodec<T>>(
765        &self,
766        component_id: ComponentId,
767        entity: EntityHandle,
768        codec: &C,
769    ) -> Result<T, ComponentStoreError> {
770        let blob = self
771            .get_blob(component_id, entity)
772            .ok_or(ComponentStoreError::MissingBlob(component_id))?;
773        codec
774            .decode(&blob.bytes)
775            .map_err(ComponentStoreError::Codec)
776    }
777
778    /// Gets a mutable opaque component blob.
779    pub fn get_blob_mut(
780        &mut self,
781        component_id: ComponentId,
782        entity: EntityHandle,
783    ) -> Option<&mut ComponentBlob> {
784        self.columns
785            .get_mut(usize::from(component_id.get()))
786            .and_then(Option::as_mut)
787            .and_then(|column| column.values.get_mut(&entity))
788    }
789
790    /// Clears dirty flags for all components on one entity.
791    pub fn clear_dirty_for_entity(&mut self, entity: EntityHandle) -> usize {
792        let mut cleared = 0;
793        for column in self.columns.iter_mut().filter_map(Option::as_mut) {
794            if let Some(blob) = column.values.get_mut(&entity)
795                && blob.dirty
796            {
797                blob.dirty = false;
798                cleared += 1;
799            }
800        }
801        cleared
802    }
803
804    /// Removes all component blobs for an entity and returns the removed values.
805    pub fn remove_entity(&mut self, entity: EntityHandle) -> Vec<(ComponentId, ComponentBlob)> {
806        let mut removed = Vec::new();
807        for (index, column) in self.columns.iter_mut().enumerate() {
808            let Some(column) = column else {
809                continue;
810            };
811            if let Some(blob) = column.values.remove(&entity) {
812                let component_id = u16::try_from(index)
813                    .expect("component columns are indexed by u16 component ids");
814                removed.push((ComponentId::new(component_id), blob));
815            }
816        }
817        removed
818    }
819
820    /// Copies migratable component blobs from one entity handle to another.
821    pub fn copy_for_migration(
822        &mut self,
823        registry: &ComponentRegistry,
824        source: EntityHandle,
825        target: EntityHandle,
826    ) -> usize {
827        let mut copied = 0;
828        for descriptor in registry.iter() {
829            if descriptor.migration != ComponentMigrationMode::Copy {
830                continue;
831            }
832            let Some(blob) = self.get_blob(descriptor.id, source).cloned() else {
833                continue;
834            };
835            self.column_mut(descriptor.id).values.insert(target, blob);
836            copied += 1;
837        }
838        copied
839    }
840
841    /// Returns number of component blobs stored in all columns.
842    pub fn blob_count(&self) -> usize {
843        self.columns
844            .iter()
845            .filter_map(Option::as_ref)
846            .map(|column| column.values.len())
847            .sum()
848    }
849
850    fn column_mut(&mut self, component_id: ComponentId) -> &mut ComponentColumn {
851        let index = usize::from(component_id.get());
852        if self.columns.len() <= index {
853            self.columns.resize_with(index + 1, || None);
854        }
855        self.columns[index].get_or_insert_with(ComponentColumn::default)
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862
863    #[test]
864    fn registry_rejects_duplicate_ids_and_names() {
865        let mut registry = ComponentRegistry::default();
866        let descriptor = ComponentDescriptor::sparse_blob(
867            ComponentId::new(1),
868            "health",
869            ComponentSyncMode::Delta,
870            ComponentMigrationMode::Copy,
871            16,
872        );
873
874        registry
875            .register(descriptor.clone())
876            .expect("first registration should work");
877        assert_eq!(
878            registry
879                .register(descriptor.clone())
880                .expect_err("duplicate id"),
881            ComponentRegistryError::DuplicateId(ComponentId::new(1))
882        );
883        assert_eq!(
884            registry
885                .register(ComponentDescriptor::sparse_blob(
886                    ComponentId::new(2),
887                    "health",
888                    ComponentSyncMode::Delta,
889                    ComponentMigrationMode::Copy,
890                    16,
891                ))
892                .expect_err("duplicate name"),
893            ComponentRegistryError::DuplicateName("health")
894        );
895    }
896
897    #[test]
898    fn component_store_sets_clears_and_migrates_blobs() {
899        let descriptor = ComponentDescriptor::sparse_blob(
900            ComponentId::new(1),
901            "health",
902            ComponentSyncMode::Delta,
903            ComponentMigrationMode::Copy,
904            16,
905        );
906        let mut registry = ComponentRegistry::default();
907        registry
908            .register(descriptor.clone())
909            .expect("descriptor should register");
910        let mut store = ComponentStore::default();
911        let source = EntityHandle::new(1, 0);
912        let target = EntityHandle::new(2, 0);
913
914        store
915            .set_blob(&descriptor, source, 7, vec![1, 2, 3])
916            .expect("blob should fit");
917        assert!(
918            store
919                .get_blob(ComponentId::new(1), source)
920                .expect("blob")
921                .dirty
922        );
923        assert_eq!(store.clear_dirty_for_entity(source), 1);
924        assert!(
925            !store
926                .get_blob(ComponentId::new(1), source)
927                .expect("blob")
928                .dirty
929        );
930
931        assert_eq!(store.copy_for_migration(&registry, source, target), 1);
932        assert_eq!(
933            store
934                .get_blob(ComponentId::new(1), target)
935                .expect("target blob")
936                .bytes,
937            vec![1, 2, 3]
938        );
939    }
940
941    #[test]
942    fn typed_component_codec_roundtrips_values() {
943        let descriptor = ComponentDescriptor::sparse_blob(
944            ComponentId::new(3),
945            "velocity",
946            ComponentSyncMode::Delta,
947            ComponentMigrationMode::Copy,
948            12,
949        )
950        .with_schema_hash(0xABCD);
951        let schema = ComponentSchema::new(descriptor.clone(), &Vec3LeCodec);
952        assert_eq!(schema.fixed_size, Some(12));
953        assert_eq!(schema.descriptor.schema_hash, 0xABCD);
954
955        let mut store = ComponentStore::default();
956        let entity = EntityHandle::new(7, 0);
957        let value = Vec3::new(1.0, 2.0, 3.5);
958
959        store
960            .set_typed(&descriptor, entity, 1, &Vec3LeCodec, &value)
961            .expect("typed set should work");
962        let decoded = store
963            .get_typed(ComponentId::new(3), entity, &Vec3LeCodec)
964            .expect("typed get should work");
965        assert_eq!(decoded, value);
966    }
967
968    #[test]
969    fn generated_schema_builds_descriptor_and_registers() {
970        const FIELDS: &[ComponentFieldDescriptor] = &[
971            ComponentFieldDescriptor::new("position", ComponentFieldType::Vec3, 0),
972            ComponentFieldDescriptor::new("health", ComponentFieldType::U32, 12),
973        ];
974        let generated = GeneratedComponentSchema::new(
975            ComponentId::new(8),
976            "unit_state",
977            ComponentStorageKind::SparseBlob,
978            ComponentSyncMode::Delta,
979            ComponentMigrationMode::Copy,
980            16,
981            FIELDS,
982        );
983
984        generated.validate().expect("schema should be valid");
985        assert_eq!(generated.fixed_size(), Some(16));
986        assert_ne!(generated.schema_hash(), 0);
987
988        let descriptor = generated.descriptor();
989        assert_eq!(descriptor.id, ComponentId::new(8));
990        assert_eq!(descriptor.schema_hash, generated.schema_hash());
991
992        let mut registry = ComponentRegistry::default();
993        let schema = registry
994            .register_generated_schema(&generated)
995            .expect("generated schema should register");
996        assert_eq!(schema.fixed_size, Some(16));
997        assert_eq!(
998            registry
999                .get(ComponentId::new(8))
1000                .expect("registered descriptor")
1001                .schema_hash,
1002            generated.schema_hash()
1003        );
1004    }
1005
1006    #[test]
1007    fn generated_schema_validation_rejects_bad_layouts() {
1008        const DUP_FIELDS: &[ComponentFieldDescriptor] = &[
1009            ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 0),
1010            ComponentFieldDescriptor::new("x", ComponentFieldType::U32, 4),
1011        ];
1012        const OVERLAP_FIELDS: &[ComponentFieldDescriptor] = &[
1013            ComponentFieldDescriptor::new("left", ComponentFieldType::U32, 0),
1014            ComponentFieldDescriptor::new("right", ComponentFieldType::U32, 2),
1015        ];
1016        const OOB_FIELDS: &[ComponentFieldDescriptor] = &[ComponentFieldDescriptor::new(
1017            "wide",
1018            ComponentFieldType::U64,
1019            4,
1020        )];
1021
1022        let duplicate = GeneratedComponentSchema::new(
1023            ComponentId::new(1),
1024            "duplicate",
1025            ComponentStorageKind::SparseBlob,
1026            ComponentSyncMode::Delta,
1027            ComponentMigrationMode::Copy,
1028            8,
1029            DUP_FIELDS,
1030        );
1031        assert_eq!(
1032            duplicate.validate().expect_err("duplicate should fail"),
1033            ComponentSchemaError::DuplicateFieldName("x")
1034        );
1035
1036        let overlap = GeneratedComponentSchema::new(
1037            ComponentId::new(2),
1038            "overlap",
1039            ComponentStorageKind::SparseBlob,
1040            ComponentSyncMode::Delta,
1041            ComponentMigrationMode::Copy,
1042            8,
1043            OVERLAP_FIELDS,
1044        );
1045        assert_eq!(
1046            overlap.validate().expect_err("overlap should fail"),
1047            ComponentSchemaError::FieldOverlap {
1048                left: "left",
1049                right: "right"
1050            }
1051        );
1052
1053        let out_of_bounds = GeneratedComponentSchema::new(
1054            ComponentId::new(3),
1055            "oob",
1056            ComponentStorageKind::SparseBlob,
1057            ComponentSyncMode::Delta,
1058            ComponentMigrationMode::Copy,
1059            8,
1060            OOB_FIELDS,
1061        );
1062        assert_eq!(
1063            out_of_bounds
1064                .validate()
1065                .expect_err("out of bounds should fail"),
1066            ComponentSchemaError::FieldOutOfBounds {
1067                name: "wide",
1068                offset: 4,
1069                size: 8,
1070                max_bytes: 8
1071            }
1072        );
1073    }
1074}