Skip to main content

cobble/
config.rs

1use crate::pack_format::{COBBLE_VERSION, SUPPORTED_MINECRAFT_VERSION, SUPPORTED_PACK_FORMAT};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct CobbleConfig {
8    pub project: ProjectConfig,
9    #[serde(default)]
10    pub build: BuildConfig,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ProjectConfig {
15    pub name: String,
16    pub description: String,
17    pub namespace: String,
18    #[serde(default = "default_version")]
19    pub version: String,
20    #[serde(default = "default_pack_format")]
21    pub pack_format: String, // Changed from u8 to String to support decimal formats
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct BuildConfig {
26    #[serde(default = "default_source")]
27    pub source: String,
28    #[serde(default = "default_output")]
29    pub output: String,
30    #[serde(default)]
31    pub entry_points: Vec<String>,
32}
33
34fn default_version() -> String {
35    "1.0.0".to_string()
36}
37
38fn default_pack_format() -> String {
39    SUPPORTED_PACK_FORMAT.to_string()
40}
41
42fn default_source() -> String {
43    "src".to_string()
44}
45
46fn default_output() -> String {
47    "output".to_string()
48}
49
50impl CobbleConfig {
51    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, String> {
52        let config = Self::load_unvalidated(path)?;
53        config.validate_pack_format()?;
54        Ok(config)
55    }
56
57    pub fn load_unvalidated<P: AsRef<Path>>(path: P) -> Result<Self, String> {
58        let contents =
59            fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
60        toml::from_str(&contents).map_err(|e| format!("Failed to parse config file: {}", e))
61    }
62
63    pub fn validate_pack_format(&self) -> Result<(), String> {
64        // Validate pack_format
65        let pack_format = crate::pack_format::PackFormat::parse_format(&self.project.pack_format)
66            .map_err(|e| {
67            format!("Invalid pack_format '{}': {}", self.project.pack_format, e)
68        })?;
69
70        if !pack_format.is_supported() {
71            return Err(format!(
72                "Invalid pack_format: {}. Must be {} (Minecraft Java Edition {}).\n\
73                 \n\
74                 Cobble v{} exclusively supports Minecraft Java Edition {}.\n\
75                 See https://minecraft.wiki/w/Pack_format for version compatibility.\n\
76                 \n\
77                 Update your cobble.toml:\n\
78                 [project]\n\
79                 pack_format = \"{}\"",
80                self.project.pack_format,
81                SUPPORTED_PACK_FORMAT,
82                SUPPORTED_MINECRAFT_VERSION,
83                COBBLE_VERSION,
84                SUPPORTED_MINECRAFT_VERSION,
85                SUPPORTED_PACK_FORMAT
86            ));
87        }
88
89        Ok(())
90    }
91
92    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
93        let contents = toml::to_string_pretty(self)
94            .map_err(|e| format!("Failed to serialize config: {}", e))?;
95        fs::write(path, contents).map_err(|e| format!("Failed to write config file: {}", e))
96    }
97
98    pub fn default_with_name(name: String) -> Self {
99        Self {
100            project: ProjectConfig {
101                namespace: name.to_lowercase().replace(" ", "_").replace("-", "_"),
102                description: format!("{} Data Pack", name),
103                name,
104                version: default_version(),
105                pack_format: default_pack_format(),
106            },
107            build: BuildConfig {
108                source: default_source(),
109                output: default_output(),
110                entry_points: vec![],
111            },
112        }
113    }
114
115    pub fn find_in_path<P: AsRef<Path>>(path: P) -> Option<PathBuf> {
116        const MAX_DEPTH: usize = 100;
117        Self::find_in_path_with_depth(path, 0, MAX_DEPTH)
118    }
119
120    fn find_in_path_with_depth<P: AsRef<Path>>(
121        path: P,
122        depth: usize,
123        max_depth: usize,
124    ) -> Option<PathBuf> {
125        if depth > max_depth {
126            return None;
127        }
128
129        let path = path.as_ref();
130        let config_file = path.join("cobble.toml");
131
132        if config_file.exists() {
133            return Some(config_file);
134        }
135
136        // Search in parent directories
137        if let Some(parent) = path.parent() {
138            if parent != path {
139                return Self::find_in_path_with_depth(parent, depth + 1, max_depth);
140            }
141        }
142
143        None
144    }
145}