use serde::{Deserialize, Serialize};
use std::fmt;
use crate::avatar::AvatarDef;
use crate::creation::CreationDef;
use crate::entity::WorldEntity;
use crate::soundtrack::SoundtrackDef;
use crate::tour::TourDef;
pub const WORLD_SCHEMA_VERSION: u32 = 3;
pub const MIN_SUPPORTED_VERSION: u32 = 3;
#[derive(Debug, Clone)]
pub enum VersionError {
TooOld { found: u32, min: u32 },
TooNew { found: u32, current: u32 },
}
impl fmt::Display for VersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionError::TooOld { found, min } => {
write!(
f,
"version {} is too old (minimum supported: {})",
found, min
)
}
VersionError::TooNew { found, current } => {
write!(
f,
"version {} is from a newer localgpt-gen (current: {})",
found, current
)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WorldManifest {
#[serde(default = "default_version")]
pub version: u32,
pub meta: WorldMeta,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub environment: Option<EnvironmentDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub camera: Option<CameraDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avatar: Option<AvatarDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tours: Vec<TourDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub soundtrack: Option<SoundtrackDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub layout_file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub region_files: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub behavior_files: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio_files: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub avatar_file: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entities: Vec<WorldEntity>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub creations: Vec<CreationDef>,
#[serde(default = "default_next_id")]
pub next_entity_id: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WorldMeta {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub biome: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub time_of_day: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variation_group: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variation: Option<(String, String)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generation_duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compliance: Option<ComplianceMeta>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ComplianceMeta {
#[serde(default = "default_true")]
pub steam_code_tool_exempt: bool,
#[serde(default = "default_risk_level")]
pub eu_ai_act_risk_level: String,
#[serde(default = "default_true")]
pub no_gen_ai_compatible: bool,
#[serde(default = "default_generation_tool")]
pub generation_tool: String,
#[serde(default = "default_generation_method")]
pub generation_method: String,
#[serde(default = "default_true")]
pub human_modifiable: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct EnvironmentDef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background_color: Option<[f32; 4]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ambient_intensity: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ambient_color: Option<[f32; 4]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fog_density: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fog_color: Option<[f32; 4]>,
#[serde(default, flatten)]
pub extra: std::collections::BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CameraDef {
#[serde(default = "default_camera_pos")]
pub position: [f32; 3],
#[serde(default)]
pub look_at: [f32; 3],
#[serde(default = "default_fov")]
pub fov_degrees: f32,
}
impl Default for CameraDef {
fn default() -> Self {
Self {
position: default_camera_pos(),
look_at: [0.0, 0.0, 0.0],
fov_degrees: default_fov(),
}
}
}
fn default_version() -> u32 {
WORLD_SCHEMA_VERSION
}
fn default_next_id() -> u64 {
1
}
fn default_camera_pos() -> [f32; 3] {
[5.0, 5.0, 5.0]
}
fn default_fov() -> f32 {
45.0
}
fn default_true() -> bool {
true
}
fn default_risk_level() -> String {
"minimal".to_string()
}
fn default_generation_tool() -> String {
"LocalGPT Gen".to_string()
}
fn default_generation_method() -> String {
"code-generation".to_string()
}
impl Default for ComplianceMeta {
fn default() -> Self {
Self {
steam_code_tool_exempt: true,
eu_ai_act_risk_level: default_risk_level(),
no_gen_ai_compatible: true,
generation_tool: default_generation_tool(),
generation_method: default_generation_method(),
human_modifiable: true,
}
}
}
impl WorldManifest {
pub fn new(name: impl Into<String>) -> Self {
Self {
version: default_version(),
meta: WorldMeta {
name: name.into(),
description: None,
biome: None,
time_of_day: None,
tags: None,
source: None,
variation_group: None,
variation: None,
prompt: None,
model: None,
generation_duration_ms: None,
style_ref: None,
compliance: Some(ComplianceMeta::default()),
},
environment: None,
camera: None,
avatar: None,
layout_file: None,
region_files: None,
behavior_files: None,
audio_files: None,
avatar_file: None,
tours: Vec::new(),
soundtrack: None,
entities: Vec::new(),
creations: Vec::new(),
next_entity_id: default_next_id(),
}
}
pub fn check_version(&self) -> Result<(), VersionError> {
if self.version < MIN_SUPPORTED_VERSION {
Err(VersionError::TooOld {
found: self.version,
min: MIN_SUPPORTED_VERSION,
})
} else if self.version > WORLD_SCHEMA_VERSION {
Err(VersionError::TooNew {
found: self.version,
current: WORLD_SCHEMA_VERSION,
})
} else {
Ok(())
}
}
pub fn alloc_entity_id(&mut self) -> crate::identity::EntityId {
let id = crate::identity::EntityId(self.next_entity_id);
self.next_entity_id += 1;
id
}
pub fn entity_count(&self) -> usize {
self.entities.len()
}
pub fn estimate_triangles(&self) -> usize {
self.entities
.iter()
.filter_map(|e| e.shape.as_ref())
.map(|s| s.estimate_triangles())
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn worlds_before_version_3_are_refused() {
let json = r#"{"version": 2, "meta": {"name": "old"}}"#;
let m: WorldManifest = serde_json::from_str(json).unwrap();
assert!(matches!(
m.check_version(),
Err(VersionError::TooOld { found: 2, min: 3 })
));
}
use crate::entity::WorldEntity;
use crate::shape::Shape;
#[test]
fn manifest_new() {
let m = WorldManifest::new("test_world");
assert_eq!(m.meta.name, "test_world");
assert_eq!(m.version, WORLD_SCHEMA_VERSION);
assert_eq!(m.next_entity_id, 1);
assert!(m.entities.is_empty());
}
#[test]
fn alloc_entity_id() {
let mut m = WorldManifest::new("test");
let id1 = m.alloc_entity_id();
let id2 = m.alloc_entity_id();
assert_eq!(id1.0, 1);
assert_eq!(id2.0, 2);
assert_eq!(m.next_entity_id, 3);
}
#[test]
fn manifest_roundtrip_json() {
let mut m = WorldManifest::new("roundtrip_test");
m.meta.description = Some("A test world".to_string());
m.environment = Some(EnvironmentDef {
background_color: Some([0.1, 0.1, 0.2, 1.0]),
ambient_intensity: Some(0.3),
ambient_color: None,
fog_density: None,
fog_color: None,
extra: std::collections::BTreeMap::new(),
});
m.camera = Some(CameraDef::default());
m.avatar = Some(AvatarDef::default());
m.entities
.push(WorldEntity::new(1, "cube").with_shape(Shape::Cuboid {
x: 2.0,
y: 2.0,
z: 2.0,
}));
m.next_entity_id = 2;
let json = serde_json::to_string_pretty(&m).unwrap();
let back: WorldManifest = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
}
#[test]
fn triangle_estimate() {
let mut m = WorldManifest::new("budget_test");
m.entities
.push(WorldEntity::new(1, "cube").with_shape(Shape::Cuboid {
x: 1.0,
y: 1.0,
z: 1.0,
}));
m.entities
.push(WorldEntity::new(2, "sphere").with_shape(Shape::Sphere { radius: 1.0 }));
assert!(m.estimate_triangles() > 0);
}
#[test]
fn version_check_current() {
let m = WorldManifest::new("test");
assert!(m.check_version().is_ok());
}
#[test]
fn version_check_too_old() {
let mut m = WorldManifest::new("test");
m.version = 0; let err = m.check_version().unwrap_err();
match err {
VersionError::TooOld { found, min } => {
assert_eq!(found, 0);
assert_eq!(min, MIN_SUPPORTED_VERSION);
}
_ => panic!("Expected TooOld error"),
}
}
#[test]
fn version_check_too_new() {
let mut m = WorldManifest::new("test");
m.version = 99; let err = m.check_version().unwrap_err();
match err {
VersionError::TooNew { found, current } => {
assert_eq!(found, 99);
assert_eq!(current, WORLD_SCHEMA_VERSION);
}
_ => panic!("Expected TooNew error"),
}
}
#[test]
fn compliance_meta_default() {
let c = ComplianceMeta::default();
assert!(c.steam_code_tool_exempt);
assert_eq!(c.eu_ai_act_risk_level, "minimal");
assert!(c.no_gen_ai_compatible);
assert!(c.generation_tool.starts_with("LocalGPT Gen")); assert_eq!(c.generation_method, "code-generation");
assert!(c.human_modifiable);
}
#[test]
fn compliance_roundtrip_json() {
let c = ComplianceMeta::default();
let json = serde_json::to_string_pretty(&c).unwrap();
let back: ComplianceMeta = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
#[test]
fn manifest_new_includes_compliance() {
let m = WorldManifest::new("compliance_test");
assert!(m.meta.compliance.is_some());
let c = m.meta.compliance.unwrap();
assert!(c.steam_code_tool_exempt);
assert_eq!(c.eu_ai_act_risk_level, "minimal");
assert!(c.no_gen_ai_compatible);
assert!(c.human_modifiable);
assert_eq!(c.generation_method, "code-generation");
}
}