1use std::collections::HashMap;
4
5use crate::ids::{ComponentId, EntityHandle};
6use crate::spatial::Vec3;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ComponentCodecError {
11 ExpectedBytes {
13 expected: usize,
15 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
32pub trait ComponentCodec<T> {
35 fn encode(&self, value: &T, out: &mut Vec<u8>) -> Result<(), ComponentCodecError>;
37
38 fn decode(&self, input: &[u8]) -> Result<T, ComponentCodecError>;
40
41 fn fixed_size(&self) -> Option<usize> {
43 None
44 }
45}
46
47#[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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum ComponentStorageKind {
132 SparseBlob,
134 External,
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum ComponentSyncMode {
141 NotReplicated,
143 Delta,
145 Snapshot,
147 EventOnly,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum ComponentMigrationMode {
154 Copy,
156 Drop,
158 External,
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct ComponentDescriptor {
165 pub id: ComponentId,
167 pub name: &'static str,
169 pub storage: ComponentStorageKind,
171 pub sync: ComponentSyncMode,
173 pub migration: ComponentMigrationMode,
175 pub max_bytes: usize,
177 pub schema_hash: u64,
179}
180
181impl ComponentDescriptor {
182 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 #[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#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct ComponentSchema {
212 pub descriptor: ComponentDescriptor,
214 pub fixed_size: Option<usize>,
216}
217
218impl ComponentSchema {
219 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum ComponentFieldType {
231 U8,
233 U16,
235 U32,
237 U64,
239 I32,
241 F32,
243 Vec3,
245 Bytes {
247 max_len: usize,
249 },
250}
251
252impl ComponentFieldType {
253 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub struct ComponentFieldDescriptor {
282 pub name: &'static str,
284 pub ty: ComponentFieldType,
286 pub offset: usize,
288}
289
290impl ComponentFieldDescriptor {
291 pub const fn new(name: &'static str, ty: ComponentFieldType, offset: usize) -> Self {
293 Self { name, ty, offset }
294 }
295
296 pub const fn end_offset(self) -> usize {
298 self.offset.saturating_add(self.ty.max_size())
299 }
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq)]
304pub struct GeneratedComponentSchema {
305 pub id: ComponentId,
307 pub name: &'static str,
309 pub storage: ComponentStorageKind,
311 pub sync: ComponentSyncMode,
313 pub migration: ComponentMigrationMode,
315 pub max_bytes: usize,
317 pub fields: &'static [ComponentFieldDescriptor],
319}
320
321impl GeneratedComponentSchema {
322 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 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 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 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 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 pub fn component_schema(&self) -> ComponentSchema {
417 ComponentSchema {
418 descriptor: self.descriptor(),
419 fixed_size: self.fixed_size(),
420 }
421 }
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq)]
426pub enum ComponentSchemaError {
427 DuplicateFieldName(&'static str),
429 FieldOutOfBounds {
431 name: &'static str,
433 offset: usize,
435 size: usize,
437 max_bytes: usize,
439 },
440 FieldOverlap {
442 left: &'static str,
444 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#[derive(Clone, Debug, PartialEq, Eq)]
473pub enum ComponentRegistryError {
474 DuplicateId(ComponentId),
476 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#[derive(Clone, Debug, PartialEq, Eq)]
493pub enum GeneratedSchemaRegistrationError {
494 Schema(ComponentSchemaError),
496 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#[derive(Clone, Debug, Default)]
520pub struct ComponentRegistry {
521 descriptors: Vec<Option<ComponentDescriptor>>,
522}
523
524impl ComponentRegistry {
525 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 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 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 pub fn iter(&self) -> impl Iterator<Item = &ComponentDescriptor> {
568 self.descriptors.iter().filter_map(Option::as_ref)
569 }
570
571 pub fn len(&self) -> usize {
573 self.iter().count()
574 }
575
576 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
638pub struct ComponentBlob {
639 pub version: u64,
641 pub dirty: bool,
643 pub bytes: Vec<u8>,
645}
646
647#[derive(Clone, Copy, Debug, PartialEq, Eq)]
649pub enum ComponentStoreError {
650 NotBlobStorage(ComponentId),
652 BlobTooLarge {
654 component_id: ComponentId,
656 actual: usize,
658 max: usize,
660 },
661 Codec(ComponentCodecError),
663 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#[derive(Clone, Debug, Default)]
699pub struct ComponentStore {
700 columns: Vec<Option<ComponentColumn>>,
701}
702
703impl ComponentStore {
704 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 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 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 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 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 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 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 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 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(®istry, 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}