use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use bevy_ecs::component::ComponentId;
use bevy_ecs::world::World;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeSet, HashMap};
use std::fs;
use std::path::{Path, PathBuf};
use crate::archetype_archive::{
ArchetypeSnapshot, StorageTypeFlag, WorldArchSnapshot,
load_world_arch_snapshot_defragment as load_world_arch_snapshot, load_world_resource,
save_world_arch_snapshot, save_world_resource, load_world_arch_snapshot_with_remap,
};
#[cfg(feature = "arrow_rs")]
use crate::arrow_snapshot::ComponentTable;
use crate::bevy_registry::{SnapshotRegistry, IDRemapRegistry, EntityRemapper};
use crate::csv_archive::ColumnarCsv;
use crate::csv_archive::columnar_from_snapshot;
use crate::traits::Archive;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuroraLocation {
File(String),
Embed(String),
Unknown(String),
}
impl Archive for AuroraWorldManifest {
fn create(
world: &World,
registry: &SnapshotRegistry,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
save_world_manifest(world, registry).map_err(|e| e.into())
}
fn apply(
&self,
world: &mut World,
registry: &SnapshotRegistry,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
load_world_manifest(world, self, registry).map_err(|e| e.into())
}
fn apply_with_remap(
&self,
world: &mut World,
registry: &SnapshotRegistry,
id_registry: &IDRemapRegistry,
mapper: &dyn EntityRemapper,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let snap: WorldArchSnapshot = self.into();
load_world_arch_snapshot_with_remap(world, &snap, registry, id_registry, mapper);
load_world_resource(&self.world.resources, world, registry);
Ok(())
}
fn get_entities(&self) -> Vec<u32> {
let snap: WorldArchSnapshot = self.into();
snap.entities
}
fn load_resources(
&self,
world: &mut World,
registry: &SnapshotRegistry,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
load_world_resource(&self.world.resources, world, registry);
Ok(())
}
fn save_to(
&self,
path: impl AsRef<Path>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let p = path.as_ref().to_str().ok_or("Invalid path")?;
let ext = path
.as_ref()
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let format = match ext.as_str() {
"json" => Some(ManifestOutputFormat::Json),
"toml" => Some(ManifestOutputFormat::Toml),
_ => None,
};
self.to_file(p, format).map_err(|e| e.into())
}
fn load_from(
path: impl AsRef<Path>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let p = path.as_ref().to_str().ok_or("Invalid path")?;
Self::from_file(p, None).map_err(|e| e.into())
}
}
impl From<&str> for AuroraLocation {
fn from(s: &str) -> Self {
if let Some(rest) = s.strip_prefix("file://") {
Self::File(rest.to_string())
} else if let Some(rest) = s.strip_prefix("embed://") {
Self::Embed(rest.to_string())
} else {
Self::Unknown(s.to_string())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuroraFormat {
Csv,
Json,
MsgPack, CsvMsgPack, #[cfg(feature = "arrow_rs")]
Parquet,
Unknown,
}
impl AuroraFormat {
pub fn from_path(path: &str) -> Self {
if path.ends_with(".csv") {
Self::Csv
} else if path.ends_with(".json") {
Self::Json
} else if path.ends_with(".csv.msgpack") {
Self::CsvMsgPack
} else if path.ends_with(".msgpack") {
Self::MsgPack
} else {
#[cfg(feature = "arrow_rs")]
{
if path.ends_with(".parquet") {
return Self::Parquet;
}
}
Self::Unknown
}
}
pub fn from_str(s: &str) -> Self {
match s {
"csv" => Self::Csv,
"json" => Self::Json,
"msgpack" => Self::MsgPack,
"csv.msgpack" => Self::CsvMsgPack,
#[cfg(feature = "arrow_rs")]
"parquet" => Self::Parquet,
_ => Self::Unknown,
}
}
}
pub struct LoadedBlob {
pub format: AuroraFormat,
pub bytes: Vec<u8>,
}
#[derive(Debug)]
pub enum AuroraInternalFormat {
ColumnarCsv(ColumnarCsv),
ArchetypeSnapshot(ArchetypeSnapshot),
#[cfg(feature = "arrow_rs")]
ArrowComponentTable(ComponentTable),
}
pub fn load_blob_from_location_with_base(
loc: &AuroraLocation,
embed_map: &HashMap<String, EmbeddedBlob>,
base_dir: &Path,
) -> Result<LoadedBlob, String> {
match loc {
AuroraLocation::File(raw_path) => {
let relative_path = Path::new(raw_path);
let full_path = if relative_path.is_absolute() {
relative_path.to_path_buf()
} else {
base_dir.join(relative_path)
};
let bytes = fs::read(&full_path)
.map_err(|e| format!("Failed to read {}: {}", full_path.display(), e))?;
let format = AuroraFormat::from_path(
full_path.file_name().and_then(|s| s.to_str()).unwrap_or(""),
);
Ok(LoadedBlob { format, bytes })
}
AuroraLocation::Embed(name) => {
let blob = embed_map.get(name).ok_or_else(|| {
format!(
"Embedded blob '{}' not found in manifest embed section.",
name
)
})?;
let format = AuroraFormat::from_str(&blob.format);
let bytes = match format {
AuroraFormat::MsgPack | AuroraFormat::CsvMsgPack => BASE64_STANDARD
.decode(&blob.data)
.map_err(|e| format!("Base64 decode failed: {}", e))?,
#[cfg(feature = "arrow_rs")]
AuroraFormat::Parquet => BASE64_STANDARD
.decode(&blob.data)
.map_err(|e| format!("Base64 decode failed: {}", e))?,
_ => blob.data.as_bytes().to_vec(),
};
Ok(LoadedBlob { format, bytes })
}
AuroraLocation::Unknown(s) => Err(format!("Unknown location type: {}", s)),
}
}
pub fn load_blob_from_location(
loc: &AuroraLocation,
embed_map: &HashMap<String, EmbeddedBlob>,
) -> Result<LoadedBlob, String> {
load_blob_from_location_with_base(loc, embed_map, Path::new("."))
}
fn parse_blob(blob: &LoadedBlob) -> Result<AuroraInternalFormat, String> {
match &blob.format {
AuroraFormat::Csv => ColumnarCsv::from_csv_reader(&blob.bytes[..])
.map(AuroraInternalFormat::ColumnarCsv)
.map_err(|e| e.to_string()),
AuroraFormat::Json => serde_json::from_slice(&blob.bytes)
.map(AuroraInternalFormat::ArchetypeSnapshot)
.map_err(|e| e.to_string()),
AuroraFormat::MsgPack => rmp_serde::from_slice(&blob.bytes)
.map(AuroraInternalFormat::ArchetypeSnapshot)
.map_err(|e| e.to_string()),
AuroraFormat::CsvMsgPack => rmp_serde::from_slice(&blob.bytes)
.map(AuroraInternalFormat::ColumnarCsv)
.map_err(|e| e.to_string()),
#[cfg(feature = "arrow_rs")]
AuroraFormat::Parquet => ComponentTable::from_parquet_u8(&blob.bytes)
.map(AuroraInternalFormat::ArrowComponentTable)
.map_err(|e| e.to_string()),
_ => Err("Cannot parse unknown format".into()),
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Url(pub String);
#[derive(Deserialize, Serialize, Debug)]
pub struct ArchetypeSpec {
#[serde(default)]
pub name: Option<String>,
pub components: Vec<String>,
pub storage: Option<Vec<StorageTypeFlag>>,
pub source: Url,
}
#[derive(Clone)]
pub enum ExportFormat {
Csv,
Json,
MsgPack,
CsvMsgPack,
#[cfg(feature = "arrow_rs")]
Parquet,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct EmbeddedBlob {
pub format: String,
pub data: String,
}
#[derive(Clone)]
pub enum OutputStrategy {
Embed(ExportFormat),
File(ExportFormat, std::path::PathBuf),
Return(ExportFormat, String),
}
#[derive(Clone)]
pub struct ExportGuidance {
pub default: OutputStrategy,
pub per_arch: HashMap<usize, OutputStrategy>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WorldWithAurora {
pub version: String,
pub name: Option<String>,
pub archetypes: Vec<ArchetypeSpec>,
#[serde(default)]
pub embed: HashMap<String, EmbeddedBlob>,
#[serde(skip)]
pub external_payloads: HashMap<String, Vec<u8>>,
pub resources: HashMap<String, serde_json::Value>,
}
fn serialize_arch_data(arch: &ArchetypeSnapshot, fmt: &ExportFormat) -> (Vec<u8>, &'static str) {
match fmt {
ExportFormat::Csv => {
let csv = columnar_from_snapshot(arch);
let mut data = Vec::new();
csv.to_csv_writer(&mut data).unwrap();
(data, "csv")
}
ExportFormat::Json => (serde_json::to_vec(arch).unwrap(), "json"),
ExportFormat::MsgPack => (rmp_serde::to_vec(arch).unwrap(), "msgpack"),
ExportFormat::CsvMsgPack => {
let csv = columnar_from_snapshot(arch);
(rmp_serde::to_vec(&csv).unwrap(), "csv.msgpack")
}
#[cfg(feature = "arrow_rs")]
ExportFormat::Parquet => {
panic!("Parquet should utilize the binary pipeline, not ArchetypeSnapshot")
}
}
}
impl WorldWithAurora {
pub fn from_guided(
world: &World,
registry: &SnapshotRegistry,
guidance: &ExportGuidance,
) -> Self {
let mut archetypes = Vec::new();
let mut embed = HashMap::new();
let mut external_payloads: HashMap<String, Vec<u8>> = HashMap::new();
let reg_comp_ids: HashMap<ComponentId, &str> = registry
.type_registry
.keys()
.filter_map(|&name| registry.comp_id_by_name(name, world).map(|cid| (cid, name)))
.collect();
for (i, arch) in world.archetypes().iter().enumerate() {
if arch.is_empty() {
continue;
}
if !arch
.components()
.iter()
.any(|x| reg_comp_ids.contains_key(x))
{
continue;
}
let strat = guidance.per_arch.get(&i).unwrap_or(&guidance.default);
let (fmt, base_path, virtual_path) = match strat {
OutputStrategy::Embed(f) => (f, None, None),
OutputStrategy::File(f, p) => (f, Some(p), None),
OutputStrategy::Return(f, v) => (f, None, Some(v.clone())),
};
let (bytes, ext) = match fmt {
#[cfg(feature = "arrow_rs")]
ExportFormat::Parquet => {
let table = crate::binary_archive::save_arrow_archetype_from_world(
world,
registry,
arch,
®_comp_ids,
)
.unwrap();
(table.to_parquet().unwrap(), "parquet")
}
_ => {
let snap = crate::archetype_archive::save_single_archetype_snapshot(
world,
arch,
registry,
®_comp_ids,
);
serialize_arch_data(&snap, fmt)
}
};
let arch_name = format!("arch_{}", i);
let (source, blob_opt) = if let Some(base) = base_path {
let filename = format!("{}.{}", arch_name, ext);
let file_path = base.join(filename);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&file_path, &bytes).unwrap();
(Url(format!("file://{}", file_path.display())), None)
} else if let Some(v_path) = virtual_path {
let filename = format!("{}.{}", arch_name, ext);
let full_path = if v_path.ends_with('/') || v_path.is_empty() {
format!("{}{}", v_path, filename)
} else {
format!("{}/{}", v_path, filename)
};
external_payloads.insert(full_path.clone(), bytes);
(Url(format!("file://{}", full_path)), None)
} else {
let data_str = match fmt {
ExportFormat::Csv | ExportFormat::Json => String::from_utf8(bytes).unwrap(),
ExportFormat::MsgPack | ExportFormat::CsvMsgPack => {
BASE64_STANDARD.encode(&bytes)
}
#[cfg(feature = "arrow_rs")]
ExportFormat::Parquet => BASE64_STANDARD.encode(&bytes),
};
let blob = EmbeddedBlob {
format: ext.to_string(),
data: data_str,
};
(Url(format!("embed://{}", arch_name)), Some(blob))
};
let components: Vec<String> = arch
.components()
.iter()
.filter_map(|id| reg_comp_ids.get(id).map(|s| s.to_string()))
.collect();
archetypes.push(ArchetypeSpec {
name: Some(arch_name.clone()),
components,
storage: None,
source,
});
if let Some(blob) = blob_opt {
embed.insert(arch_name, blob);
}
}
Self {
version: "0.1".into(),
archetypes,
embed,
external_payloads,
name: None,
resources: HashMap::new(),
}
}
}
impl From<&WorldArchSnapshot> for WorldWithAurora {
fn from(world: &WorldArchSnapshot) -> Self {
let mut archetypes = Vec::new();
let mut embed = HashMap::new();
for (i, arch) in world.archetypes.iter().enumerate() {
if arch.is_empty() {
continue;
}
let name = Some(format!("arch_{}", i));
let source = Url(format!("embed://arch_{}", i));
let (bytes, _ext) = serialize_arch_data(arch, &ExportFormat::Csv);
let blob = EmbeddedBlob {
format: "csv".to_string(),
data: String::from_utf8(bytes).unwrap(),
};
embed.insert(format!("arch_{}", i), blob);
archetypes.push(ArchetypeSpec {
name,
components: arch.component_types.clone(),
storage: None,
source,
});
}
Self {
version: "0.1".into(),
archetypes,
embed,
external_payloads: HashMap::new(),
name: None,
resources: HashMap::new(),
}
}
}
impl From<&AuroraWorldManifest> for WorldArchSnapshot {
fn from(manifest: &AuroraWorldManifest) -> Self {
(&manifest.world).into()
}
}
impl From<&WorldWithAurora> for WorldArchSnapshot {
#[allow(unreachable_patterns)]
fn from(world: &WorldWithAurora) -> Self {
let mut archetypes = Vec::new();
let mut all_entities: BTreeSet<u32> = BTreeSet::new();
for arch in &world.archetypes {
let loc = AuroraLocation::from(arch.source.0.as_str());
let blob = load_blob_from_location(&loc, &world.embed).unwrap();
let parsed = parse_blob(&blob).unwrap();
let snapshot = match parsed {
AuroraInternalFormat::ColumnarCsv(csv) => {
let mut snap: ArchetypeSnapshot = (&csv).into();
snap.storage_types =
arch.storage
.clone()
.unwrap_or(vec![StorageTypeFlag::Table; snap.component_types.len()]);
snap
}
AuroraInternalFormat::ArchetypeSnapshot(data) => data,
_ => panic!("not supported"),
};
all_entities.extend(snapshot.entities.clone());
archetypes.push(snapshot);
}
WorldArchSnapshot {
entities: all_entities.into_iter().collect(),
archetypes,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct AuroraWorldManifest {
pub metadata: Option<HashMap<String, Value>>,
pub world: WorldWithAurora,
}
#[derive(Default)]
pub enum ManifestOutputFormat {
Json,
#[default]
Toml,
}
impl ExportGuidance {
pub fn embed_all(format: ExportFormat) -> Self {
Self {
default: OutputStrategy::Embed(format),
per_arch: HashMap::new(),
}
}
pub fn file_all(format: ExportFormat, base_path: impl Into<PathBuf>) -> Self {
let base = base_path.into();
Self {
default: OutputStrategy::File(format.clone(), base),
per_arch: HashMap::new(),
}
}
pub fn set_strategy_for(&mut self, index: usize, strategy: OutputStrategy) -> &mut Self {
self.per_arch.insert(index, strategy);
self
}
pub fn embed_as(&mut self, index: usize, fmt: ExportFormat) -> &mut Self {
self.set_strategy_for(index, OutputStrategy::Embed(fmt))
}
pub fn file_as(
&mut self,
index: usize,
fmt: ExportFormat,
path: impl Into<PathBuf>,
) -> &mut Self {
self.set_strategy_for(index, OutputStrategy::File(fmt, path.into()))
}
pub fn get_strategy(&self, index: usize) -> OutputStrategy {
self.per_arch
.get(&index)
.cloned()
.unwrap_or_else(|| self.default.clone())
}
}
impl AuroraWorldManifest {
pub fn to_file(&self, path: &str, format: Option<ManifestOutputFormat>) -> Result<(), String> {
write_manifest_to_file(self, path, format.unwrap_or_default())
}
pub fn from_file(path: &str, format: Option<ManifestOutputFormat>) -> Result<Self, String> {
read_manifest_from_file(path, format)
}
}
pub fn save_world_manifest(
world: &World,
registry: &SnapshotRegistry,
) -> Result<AuroraWorldManifest, String> {
let snapshot = save_world_arch_snapshot(world, registry);
let mut world_with_aurora = WorldWithAurora::from(&snapshot);
world_with_aurora.resources = save_world_resource(world, registry);
Ok(AuroraWorldManifest {
metadata: None,
world: world_with_aurora,
})
}
enum LoadedArchetype {
Legacy(ArchetypeSnapshot),
#[cfg(feature = "arrow_rs")]
Arrow(ComponentTable),
}
pub trait BlobLoader {
fn load_blob(&mut self, path: &str) -> Result<Vec<u8>, String>;
}
pub struct FsBlobLoader {
pub base_dir: PathBuf,
}
impl BlobLoader for FsBlobLoader {
fn load_blob(&mut self, path: &str) -> Result<Vec<u8>, String> {
let relative_path = Path::new(path);
let full_path = if relative_path.is_absolute() {
relative_path.to_path_buf()
} else {
self.base_dir.join(relative_path)
};
fs::read(&full_path).map_err(|e| format!("Failed to read {}: {}", full_path.display(), e))
}
}
#[cfg(feature = "arrow_rs")]
pub struct ZipBlobLoader<R: std::io::Read + std::io::Seek> {
pub archive: zip::ZipArchive<R>,
}
#[cfg(feature = "arrow_rs")]
impl<R: std::io::Read + std::io::Seek> BlobLoader for ZipBlobLoader<R> {
fn load_blob(&mut self, path: &str) -> Result<Vec<u8>, String> {
use std::io::Read;
let mut file = self.archive.by_name(path).map_err(|e| e.to_string())?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).map_err(|e| e.to_string())?;
Ok(buf)
}
}
pub fn load_world_manifest_with_loader<L: BlobLoader>(
world: &mut World,
manifest: &AuroraWorldManifest,
registry: &SnapshotRegistry,
loader: &mut L,
) -> Result<(), String> {
let resource = &manifest.world.resources;
load_world_resource(resource, world, registry);
let mut loaded_archetypes = Vec::new();
for arch in &manifest.world.archetypes {
let loc = AuroraLocation::from(arch.source.0.as_str());
let blob = match loc {
AuroraLocation::File(path) => {
let bytes = loader.load_blob(&path)?;
let format = AuroraFormat::from_path(&path);
LoadedBlob { format, bytes }
}
AuroraLocation::Embed(name) => {
let blob =
manifest.world.embed.get(&name).ok_or_else(|| {
format!("Embedded blob '{}' not found in manifest.", name)
})?;
let format = AuroraFormat::from_str(&blob.format);
let bytes = match format {
AuroraFormat::MsgPack | AuroraFormat::CsvMsgPack => BASE64_STANDARD
.decode(&blob.data)
.map_err(|e| format!("Base64 decode failed: {}", e))?,
#[cfg(feature = "arrow_rs")]
AuroraFormat::Parquet => BASE64_STANDARD
.decode(&blob.data)
.map_err(|e| format!("Base64 decode failed: {}", e))?,
_ => blob.data.as_bytes().to_vec(),
};
LoadedBlob { format, bytes }
}
AuroraLocation::Unknown(s) => return Err(format!("Unknown location: {}", s)),
};
let parsed = parse_blob(&blob).unwrap();
match parsed {
AuroraInternalFormat::ColumnarCsv(csv) => {
let mut snap: ArchetypeSnapshot = (&csv).into();
snap.storage_types = arch
.storage
.clone()
.unwrap_or(vec![StorageTypeFlag::Table; snap.component_types.len()]);
loaded_archetypes.push(LoadedArchetype::Legacy(snap));
}
AuroraInternalFormat::ArchetypeSnapshot(data) => {
loaded_archetypes.push(LoadedArchetype::Legacy(data));
}
#[cfg(feature = "arrow_rs")]
AuroraInternalFormat::ArrowComponentTable(table) => {
loaded_archetypes.push(LoadedArchetype::Arrow(table));
}
}
}
let mut max_entity = 0;
for arch in &loaded_archetypes {
let max = match arch {
LoadedArchetype::Legacy(s) => s.entities.iter().max().copied().unwrap_or(0),
#[cfg(feature = "arrow_rs")]
LoadedArchetype::Arrow(t) => t.entities.iter().map(|e| e.id).max().unwrap_or(0),
};
if max > max_entity {
max_entity = max;
}
}
world.entities().reserve_entities(max_entity + 1);
world.flush();
#[cfg(feature = "arrow_rs")]
let mut buffer = crate::bevy_cmdbuffer::HarvardCommandBuffer::new();
for arch in loaded_archetypes {
match arch {
LoadedArchetype::Legacy(snap) => {
let temp_snap = WorldArchSnapshot {
entities: vec![], archetypes: vec![snap],
};
load_world_arch_snapshot(world, &temp_snap, registry);
}
#[cfg(feature = "arrow_rs")]
LoadedArchetype::Arrow(table) => {
crate::binary_archive::load_arrow_archetype_to_world(
world, ®istry, &table, &mut buffer,
)
.map_err(|e| e.to_string())?;
buffer.apply(world);
buffer.reset();
}
}
}
Ok(())
}
pub fn load_world_manifest(
world: &mut World,
manifest: &AuroraWorldManifest,
registry: &SnapshotRegistry,
) -> Result<(), String> {
let mut loader = FsBlobLoader {
base_dir: Path::new(".").to_path_buf(),
};
load_world_manifest_with_loader(world, manifest, registry, &mut loader)
}
pub fn write_manifest_to_file<P: AsRef<Path>>(
manifest: &AuroraWorldManifest,
path: P,
format: ManifestOutputFormat,
) -> Result<(), String> {
let content = match format {
ManifestOutputFormat::Json => {
serde_json::to_string_pretty(manifest).map_err(|e| e.to_string())?
}
ManifestOutputFormat::Toml => {
toml::to_string_pretty(manifest).map_err(|e| e.to_string())?
}
};
fs::write(path, content).map_err(|e| e.to_string())
}
pub fn read_manifest_from_file<P: AsRef<Path>>(
path: P,
format_hint: Option<ManifestOutputFormat>,
) -> Result<AuroraWorldManifest, String> {
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
let format = match format_hint {
Some(f) => f,
None => {
let ext = path
.as_ref()
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
match ext.as_str() {
"json" => ManifestOutputFormat::Json,
"toml" => ManifestOutputFormat::Toml,
_ => return Err(format!("Cannot guess format from extension: {}", ext)),
}
}
};
match format {
ManifestOutputFormat::Json => serde_json::from_str(&content).map_err(|e| e.to_string()),
ManifestOutputFormat::Toml => toml::from_str(&content).map_err(|e| e.to_string()),
}
}
pub fn save_world_manifest_with_guidance(
world: &World,
registry: &SnapshotRegistry,
guidance: &ExportGuidance,
) -> Result<AuroraWorldManifest, String> {
let mut world_with_aurora = WorldWithAurora::from_guided(world, registry, guidance);
world_with_aurora.resources = save_world_resource(world, registry);
Ok(AuroraWorldManifest {
metadata: None,
world: world_with_aurora,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archetype_archive::load_world_arch_snapshot;
use crate::archetype_archive::save_world_arch_snapshot;
use bevy_ecs::prelude::*;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Component)]
struct TestComponentA {
pub value: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Component)]
struct TestComponentB {
pub value: f32,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Component)]
struct TestComponentC {
pub value: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Component)]
struct TestComponentD {
pub value: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Component)]
struct TestComponentE(Vec<f64>);
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Component)]
struct TestComponentF(TestComponentC);
fn init_world() -> (World, SnapshotRegistry) {
let mut world = World::new();
let mut registry = SnapshotRegistry::default();
registry.register::<TestComponentA>();
registry.register::<TestComponentB>();
registry.register::<TestComponentC>();
registry.register::<TestComponentD>();
registry.register::<TestComponentE>();
registry.register::<TestComponentF>();
for i in 0..10 {
world.spawn((
TestComponentA { value: i },
TestComponentB {
value: i as f32 * 0.1,
},
));
world.spawn((
TestComponentB {
value: i as f32 * 0.2,
},
TestComponentC {
value: format!("EntityC{}", i),
},
));
world.spawn((
TestComponentA { value: i * 2 },
TestComponentC {
value: format!("EntityAC{}", i),
},
TestComponentD { value: i % 2 == 0 },
));
world.spawn((
TestComponentD { value: i % 3 == 0 },
TestComponentE(vec![i as f64, i as f64 + 1.0]),
));
world.spawn((
TestComponentA { value: -i },
TestComponentB {
value: -i as f32 * 0.3,
},
TestComponentC {
value: format!("Combo{}", i),
},
TestComponentD { value: i % 5 == 0 },
TestComponentE(vec![0.0; i as usize % 10 + 1]),
TestComponentF(TestComponentC {
value: format!("Nested{}", i),
}),
));
}
(world, registry)
}
#[test]
fn test_aurora_snapshot_roundtrip() {
let (world, registry) = init_world();
let snapshot = save_world_arch_snapshot(&world, ®istry);
let table = WorldWithAurora::from(&snapshot);
let data = toml::to_string_pretty(&table).unwrap();
println!("Serialized data: {}", data);
let deserialized: WorldWithAurora = toml::from_str(&data).unwrap();
let mut world2 = World::new();
load_world_arch_snapshot(&mut world2, &((&deserialized).into()), ®istry);
}
#[test]
fn test_aurora_manifest_snapshot_roundtrip() {
let (world, registry) = init_world();
let snapshot = save_world_manifest(&world, ®istry).unwrap();
let toml = toml::to_string_pretty(&snapshot).unwrap();
let deserialized: AuroraWorldManifest =
toml::from_str(&toml).expect("Failed to deserialize TOML");
let mut world2 = World::new();
load_world_manifest(&mut world2, &snapshot, ®istry).unwrap();
load_world_manifest(&mut world2, &deserialized, ®istry).unwrap();
}
#[test]
fn test_msgpack_manifest_snapshot_roundtrip() {
let path = "test_msgpack.toml";
let (world, registry) = init_world();
let guide = ExportGuidance::embed_all(ExportFormat::MsgPack);
let snapshot = save_world_manifest_with_guidance(&world, ®istry, &guide).unwrap();
snapshot.to_file(path, None).unwrap();
assert!(Path::new(path).exists(), "File not written");
let toml = fs::read_to_string(path).unwrap();
let deserialized: AuroraWorldManifest =
toml::from_str(&toml).expect("Failed to deserialize TOML");
let mut world2 = World::new();
load_world_manifest(&mut world2, &snapshot, ®istry).unwrap();
load_world_manifest(&mut world2, &deserialized, ®istry).unwrap();
fs::remove_file(path).ok();
}
#[test]
fn test_msgpack_manifest_snapshot_roundtrip_file() {
let path = "test.toml";
let arch_type_path = "arch_default_msgpack";
let (world, registry) = init_world();
let guide = ExportGuidance::file_all(ExportFormat::MsgPack, arch_type_path);
let snapshot = save_world_manifest_with_guidance(&world, ®istry, &guide).unwrap();
snapshot.to_file(path, None).unwrap();
assert!(Path::new(path).exists(), "File not written");
let toml = fs::read_to_string(path).unwrap();
let deserialized: AuroraWorldManifest =
toml::from_str(&toml).expect("Failed to deserialize TOML");
let mut world2 = World::new();
load_world_manifest(&mut world2, &snapshot, ®istry).unwrap();
load_world_manifest(&mut world2, &deserialized, ®istry).unwrap();
fs::remove_file(path).ok();
fs::remove_dir_all(arch_type_path).ok();
}
#[test]
fn test_csv_msgpack_manifest_snapshot_roundtrip() {
let path = "test_csvmsgpack.toml";
let (world, registry) = init_world();
let guide = ExportGuidance::embed_all(ExportFormat::CsvMsgPack);
let snapshot = save_world_manifest_with_guidance(&world, ®istry, &guide).unwrap();
snapshot.to_file(path, None).unwrap();
assert!(Path::new(path).exists(), "File not written");
let toml = fs::read_to_string(path).unwrap();
let deserialized: AuroraWorldManifest =
toml::from_str(&toml).expect("Failed to deserialize TOML");
let mut world2 = World::new();
load_world_manifest(&mut world2, &snapshot, ®istry).unwrap();
load_world_manifest(&mut world2, &deserialized, ®istry).unwrap();
fs::remove_file(path).ok();
}
#[test]
fn test_csv_msgpack_manifest_snapshot_roundtrip_file() {
let path = "test_csvmsgpack_file.toml";
let arch_type_path = "arch_default_csvmsgpack";
let (world, registry) = init_world();
let guide = ExportGuidance::file_all(ExportFormat::CsvMsgPack, arch_type_path);
let snapshot = save_world_manifest_with_guidance(&world, ®istry, &guide).unwrap();
snapshot.to_file(path, None).unwrap();
assert!(Path::new(path).exists(), "File not written");
let toml = fs::read_to_string(path).unwrap();
let deserialized: AuroraWorldManifest =
toml::from_str(&toml).expect("Failed to deserialize TOML");
let mut world2 = World::new();
load_world_manifest(&mut world2, &snapshot, ®istry).unwrap();
load_world_manifest(&mut world2, &deserialized, ®istry).unwrap();
fs::remove_file(path).ok();
fs::remove_dir_all(arch_type_path).ok();
}
#[test]
#[cfg(feature = "arrow_rs")]
fn test_parquet_manifest_snapshot_roundtrip() {
let path = "test_parquet.toml";
let (world, registry) = init_world();
let guide = ExportGuidance::embed_all(ExportFormat::Parquet);
let snapshot = save_world_manifest_with_guidance(&world, ®istry, &guide).unwrap();
snapshot.to_file(path, None).unwrap();
assert!(Path::new(path).exists(), "File not written");
let toml = fs::read_to_string(path).unwrap();
let deserialized: AuroraWorldManifest =
toml::from_str(&toml).expect("Failed to deserialize TOML");
let mut world2 = World::new();
load_world_manifest(&mut world2, &snapshot, ®istry).unwrap();
load_world_manifest(&mut world2, &deserialized, ®istry).unwrap();
fs::remove_file(path).ok();
}
}