use serde::{Deserialize, Serialize};
pub const GAME_VERSION: &str = "CL0";
pub const ZERO_GUID: &str = "00000000-0000-0000-0000-000000000000";
#[derive(Debug, Clone, Serialize)]
pub struct BundleAuthor {
#[serde(rename = "iD")]
pub id: String,
pub name: String,
}
impl<'de> Deserialize<'de> for BundleAuthor {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Object {
#[serde(rename = "iD")]
id: String,
name: String,
},
Name(String),
}
Ok(match Repr::deserialize(d)? {
Repr::Object { id, name } => BundleAuthor { id, name },
Repr::Name(name) => BundleAuthor {
id: ZERO_GUID.to_string(),
name,
},
})
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BundleColor {
pub r: f64,
pub g: f64,
pub b: f64,
pub a: f64,
}
impl Default for BundleColor {
fn default() -> Self {
Self {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BundleJson {
#[serde(rename = "type")]
pub level_type: String,
#[serde(rename = "iD")]
pub id: String,
pub name: String,
pub version: String,
pub tags: Vec<String>,
pub authors: Vec<BundleAuthor>,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "updatedAt")]
pub updated_at: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<BundleColor>,
pub dependencies: Vec<serde_json::Value>,
#[serde(rename = "gameVersion")]
pub game_version: String,
}
impl Default for BundleJson {
fn default() -> Self {
Self {
level_type: "World".to_string(),
id: ZERO_GUID.to_string(),
name: "".to_string(),
version: "".to_string(),
tags: vec![],
authors: vec![],
created_at: "0001.01.01-00.00.00".to_string(),
updated_at: "0001.01.01-00.00.00".to_string(),
description: "A Generated World".to_string(),
color: None,
dependencies: vec![],
game_version: GAME_VERSION.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WorldJson {
pub environment: String,
}
impl Default for WorldJson {
fn default() -> Self {
Self {
environment: "Plate".to_string(),
}
}
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
pub struct PrefabVec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
pub struct PrefabIntVec3 {
pub x: i32,
pub y: i32,
pub z: i32,
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
pub struct PrefabPivot {
pub center: PrefabVec3,
#[serde(rename = "halfExtent")]
pub half_extent: PrefabVec3,
}
impl PrefabPivot {
pub fn from_bounds(min: crate::wrapper::Position, max: crate::wrapper::Position) -> Self {
Self {
center: PrefabVec3 {
x: (min.x + max.x) as f64 / 2.0,
y: (min.y + max.y) as f64 / 2.0,
z: (min.z + max.z) as f64 / 2.0,
},
half_extent: PrefabVec3 {
x: (max.x - min.x) as f64 / 2.0,
y: (max.y - min.y) as f64 / 2.0,
z: (max.z - min.z) as f64 / 2.0,
},
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PrefabPivots {
#[serde(rename = "bottomStudsPivot")]
pub bottom_studs_pivot: PrefabPivot,
#[serde(rename = "studsExpandedPivot")]
pub studs_expanded_pivot: PrefabPivot,
#[serde(rename = "topStudsPivot")]
pub top_studs_pivot: PrefabPivot,
#[serde(rename = "boundsPivot")]
pub bounds_pivot: PrefabPivot,
#[serde(rename = "bottomStudsDirection")]
pub bottom_studs_direction: String,
#[serde(rename = "topStudsDirection")]
pub top_studs_direction: String,
#[serde(rename = "bBottomStudsValid")]
pub bottom_studs_valid: bool,
#[serde(rename = "bTopStudsValid")]
pub top_studs_valid: bool,
}
impl Default for PrefabPivots {
fn default() -> Self {
Self {
bottom_studs_pivot: PrefabPivot::default(),
studs_expanded_pivot: PrefabPivot::default(),
top_studs_pivot: PrefabPivot::default(),
bounds_pivot: PrefabPivot::default(),
bottom_studs_direction: "Z_Negative".to_string(),
top_studs_direction: "Z_Positive".to_string(),
bottom_studs_valid: true,
top_studs_valid: true,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PrefabJson {
pub pivots: PrefabPivots,
#[serde(rename = "addedGlobalGridOffset")]
pub added_global_grid_offset: PrefabIntVec3,
#[serde(rename = "bIsPhysicsGrid")]
pub is_physics_grid: bool,
#[serde(rename = "bFreezePhysicsGrid")]
pub freeze_physics_grid: bool,
#[serde(rename = "bFreezeGlobalGrid")]
pub freeze_global_grid: bool,
#[serde(rename = "bIsMicrochipPrefab")]
pub is_microchip_prefab: bool,
}
impl Default for PrefabJson {
fn default() -> Self {
Self {
pivots: PrefabPivots::default(),
added_global_grid_offset: PrefabIntVec3::default(),
is_physics_grid: false,
freeze_physics_grid: false,
freeze_global_grid: false,
is_microchip_prefab: false,
}
}
}
impl PrefabJson {
pub fn from_bounds(min: crate::wrapper::Position, max: crate::wrapper::Position) -> Self {
let pivot = PrefabPivot::from_bounds(min, max);
Self {
pivots: PrefabPivots {
bottom_studs_pivot: pivot,
studs_expanded_pivot: pivot,
top_studs_pivot: pivot,
bounds_pivot: pivot,
..Default::default()
},
..Default::default()
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct WorldMeta {
pub bundle: BundleJson,
pub screenshot: Option<Vec<u8>>,
pub thumbnail: Option<Vec<u8>>,
pub world: WorldJson,
pub prefab: Option<PrefabJson>,
}
#[cfg(test)]
mod tests {
use super::*;
const GAME_BUNDLE: &str = r#"{
"type": "Prefab",
"iD": "00000000-0000-0000-0000-000000000000",
"name": "1x1f spawner",
"version": "",
"tags": [],
"authors": [{ "iD": "12345678-9abc-4def-8123-456789abcdef", "name": "author" }],
"createdAt": "2026.07.07-17.38.40",
"updatedAt": "2026.07.07-17.38.40",
"description": "",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"dependencies": [],
"gameVersion": "CL0"
}"#;
#[test]
fn parses_game_bundle_json() {
let b: BundleJson = serde_json::from_str(GAME_BUNDLE).unwrap();
assert_eq!(b.level_type, "Prefab");
assert_eq!(b.authors.len(), 1);
assert_eq!(b.authors[0].id, "12345678-9abc-4def-8123-456789abcdef");
assert_eq!(b.authors[0].name, "author");
let c = b.color.unwrap();
assert_eq!((c.r, c.g, c.b, c.a), (1.0, 1.0, 1.0, 1.0));
}
#[test]
fn parses_legacy_string_authors_and_missing_color() {
let b: BundleJson = serde_json::from_str(
r#"{"type":"World","iD":"00000000-0000-0000-0000-000000000000",
"name":"x","version":"","tags":[],"authors":["author"],
"createdAt":"0001.01.01-00.00.00","updatedAt":"0001.01.01-00.00.00",
"description":"","dependencies":[],"gameVersion":"CL0"}"#,
)
.unwrap();
assert_eq!(b.authors[0].name, "author");
assert_eq!(b.authors[0].id, ZERO_GUID);
assert!(b.color.is_none());
}
#[test]
fn default_serialization_is_unchanged() {
let json = serde_json::to_string(&BundleJson::default()).unwrap();
assert!(json.contains(r#""authors":[]"#));
assert!(!json.contains("color"));
}
#[test]
fn author_round_trip() {
let b = BundleJson {
authors: vec![BundleAuthor {
id: "12345678-9abc-4def-8123-456789abcdef".into(),
name: "author".into(),
}],
color: Some(BundleColor::default()),
..Default::default()
};
let json = serde_json::to_string(&b).unwrap();
assert!(json.contains(
r#""authors":[{"iD":"12345678-9abc-4def-8123-456789abcdef","name":"author"}]"#
));
let back: BundleJson = serde_json::from_str(&json).unwrap();
assert_eq!(back.authors[0].name, "author");
assert!(back.color.is_some());
}
}