Skip to main content

conduit_cli/core/io/server/
config.rs

1use std::{collections::HashMap, fs, path::Path};
2
3use crate::core::error::{CoreError, CoreResult};
4use mc_gate::WakeupCondition as McWakeupCondition;
5use serde::{Deserialize, Serialize};
6
7#[derive(Serialize, Deserialize, Debug, Clone)]
8pub struct ServerConfig {
9    pub server: ServerSettings,
10    pub spawning: SpawningSettings,
11    pub network: NetworkSettings,
12    pub access: AccessSettings,
13    pub on_demand: OnDemandSettings,
14    pub resource_pack: ResourcePackSettings,
15    pub performance: PerformanceSettings,
16    pub web: WebSettings,
17}
18
19#[derive(Serialize, Deserialize, Debug, Clone)]
20#[serde(rename_all = "lowercase")]
21pub enum Difficulty {
22    Peaceful,
23    Easy,
24    Normal,
25    Hard,
26}
27
28#[derive(Serialize, Deserialize, Debug, Clone)]
29#[allow(clippy::struct_excessive_bools)]
30pub struct ServerSettings {
31    pub name: String,
32    pub level_type: String,
33    pub seed: String,
34    pub generate_structures: bool,
35    pub allow_nether: bool,
36    pub hardcore: bool,
37    pub difficulty: Difficulty,
38    pub gamemode: String,
39    pub force_gamemode: bool,
40    pub pvp: bool,
41}
42
43#[derive(Serialize, Deserialize, Debug, Clone)]
44pub struct SpawningSettings {
45    pub monsters: bool,
46    pub animals: bool,
47    pub npcs: bool,
48    pub spawn_protection: u32,
49}
50
51#[derive(Serialize, Deserialize, Debug, Clone)]
52#[serde(rename_all = "lowercase")]
53pub enum Tunnel {
54    Playit,
55    None,
56}
57
58#[derive(Serialize, Deserialize, Debug, Clone)]
59pub struct NetworkSettings {
60    pub minecraft_port: u16,
61    pub max_players: u32,
62    pub online_mode: bool,
63    pub prevent_proxy_connections: bool,
64    pub enforce_secure_profile: bool,
65    pub compression_threshold: i32,
66    pub tunnel_type: Tunnel,
67    pub motd: String,
68}
69
70#[derive(Serialize, Deserialize, Debug, Clone)]
71pub struct AccessSettings {
72    pub whitelist: bool,
73    pub op_permission_level: u8,
74    pub function_permission_level: u8,
75    pub player_idle_timeout: u32,
76    pub enable_command_block: bool,
77}
78
79#[derive(Serialize, Deserialize, Debug, Clone)]
80#[serde(rename_all = "lowercase")]
81pub enum WakeupConditionMirror {
82    Motd,
83    Join,
84    Disabled,
85}
86
87impl From<WakeupConditionMirror> for McWakeupCondition {
88    fn from(mirror: WakeupConditionMirror) -> Self {
89        match mirror {
90            WakeupConditionMirror::Motd => McWakeupCondition::Motd,
91            WakeupConditionMirror::Join => McWakeupCondition::Join,
92            WakeupConditionMirror::Disabled => McWakeupCondition::Disabled,
93        }
94    }
95}
96
97#[derive(Serialize, Deserialize, Debug, Clone)]
98pub struct OnDemandSettings {
99    pub enabled: bool,
100    pub proxy_port: u16,
101    pub idle_timedout: String,
102    pub wakeup_on: WakeupConditionMirror,
103    pub sleeping_motd: String,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone)]
107pub struct ResourcePackSettings {
108    pub url: String,
109    pub hash: String,
110    pub required: bool,
111}
112
113#[derive(Serialize, Deserialize, Debug, Clone)]
114pub struct PerformanceSettings {
115    pub view_distance: u32,
116    pub simulation_distance: u32,
117    pub entity_range: u32,
118    pub sync_chunk_writes: bool,
119    pub max_tick_time: u64,
120    pub min_ram: String,
121    pub max_ram: String,
122    pub jvm_args: Vec<String>,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone)]
126pub struct WebSettings {
127    pub enabled: bool,
128    pub bind: String,
129}
130
131impl ServerConfig {
132    pub fn load<P: AsRef<std::path::Path>>(path: P) -> CoreResult<Self> {
133        let content = std::fs::read_to_string(path)
134            .map_err(|_| CoreError::RuntimeError("Could not find config.toml".into()))?;
135        let config: ServerConfig = toml::from_str(&content)
136            .map_err(|e| CoreError::RuntimeError(format!("Error in config.toml: {e}")))?;
137        Ok(config)
138    }
139
140    pub fn load_or_create<P: AsRef<std::path::Path>>(path: P) -> CoreResult<Self> {
141        let path_ref = path.as_ref();
142
143        if !path_ref.exists() {
144            let default_config = Self::default();
145            default_config.save(path_ref)?;
146            return Ok(default_config);
147        }
148
149        Self::load(path_ref)
150    }
151
152    pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> CoreResult<()> {
153        let content =
154            toml::to_string_pretty(self).map_err(|e| CoreError::RuntimeError(e.to_string()))?;
155        std::fs::write(path, content)?;
156        Ok(())
157    }
158
159    pub fn patch_properties<P: AsRef<Path>>(&self, path: P) -> CoreResult<()> {
160        let content = fs::read_to_string(&path)
161            .map_err(|_| CoreError::RuntimeError("Could not read server.properties".into()))?;
162
163        let mut updates: HashMap<&str, String> = HashMap::new();
164
165        updates.insert("server-port", self.network.minecraft_port.to_string());
166        updates.insert("max-players", self.network.max_players.to_string());
167        updates.insert("online-mode", self.network.online_mode.to_string());
168        updates.insert("motd", self.network.motd.clone());
169        updates.insert(
170            "network-compression-threshold",
171            self.network.compression_threshold.to_string(),
172        );
173
174        updates.insert("level-name", self.server.name.clone());
175        updates.insert(
176            "level-type",
177            format!("minecraft\\:{}", self.server.level_type),
178        );
179        updates.insert("level-seed", self.server.seed.clone());
180        updates.insert(
181            "difficulty",
182            format!("{:?}", self.server.difficulty).to_lowercase(),
183        );
184        updates.insert("hardcore", self.server.hardcore.to_string());
185        updates.insert("pvp", self.server.pvp.to_string());
186
187        updates.insert("view-distance", self.performance.view_distance.to_string());
188        updates.insert(
189            "simulation-distance",
190            self.performance.simulation_distance.to_string(),
191        );
192        updates.insert(
193            "sync-chunk-writes",
194            self.performance.sync_chunk_writes.to_string(),
195        );
196
197        let mut new_lines = Vec::new();
198        let mut applied_keys = std::collections::HashSet::new();
199
200        for line in content.lines() {
201            if line.starts_with('#') || !line.contains('=') {
202                new_lines.push(line.to_string());
203                continue;
204            }
205
206            let parts: Vec<&str> = line.splitn(2, '=').collect();
207            let key = parts[0].trim();
208
209            if let Some(new_value) = updates.get(key) {
210                new_lines.push(format!("{key}={new_value}"));
211                applied_keys.insert(key.to_string());
212            } else {
213                new_lines.push(line.to_string());
214            }
215        }
216
217        for (key, value) in updates {
218            if !applied_keys.contains(key) {
219                new_lines.push(format!("{key}={value}"));
220            }
221        }
222
223        fs::write(path, new_lines.join("\n"))?;
224
225        Ok(())
226    }
227}
228
229impl Default for ServerConfig {
230    fn default() -> Self {
231        Self {
232            server: ServerSettings {
233                name: "world".into(),
234                level_type: "default".into(),
235                seed: String::new(),
236                generate_structures: true,
237                allow_nether: true,
238                hardcore: false,
239                difficulty: Difficulty::Normal,
240                gamemode: "survival".into(),
241                force_gamemode: false,
242                pvp: true,
243            },
244            spawning: SpawningSettings {
245                monsters: true,
246                animals: true,
247                npcs: true,
248                spawn_protection: 16,
249            },
250            network: NetworkSettings {
251                minecraft_port: 25565,
252                max_players: 20,
253                online_mode: true,
254                prevent_proxy_connections: false,
255                enforce_secure_profile: true,
256                compression_threshold: 256,
257                tunnel_type: Tunnel::None,
258                motd: "§bA Minecraft Server §7| §ePowered by Conduit".into(),
259            },
260            access: AccessSettings {
261                whitelist: false,
262                op_permission_level: 4,
263                function_permission_level: 2,
264                player_idle_timeout: 0,
265                enable_command_block: false,
266            },
267            on_demand: OnDemandSettings {
268                enabled: false,
269                proxy_port: 25567,
270                idle_timedout: "10m".into(),
271                wakeup_on: WakeupConditionMirror::Motd,
272                sleeping_motd: "§c§l⚡ §eStarting server...".into(),
273            },
274            resource_pack: ResourcePackSettings {
275                url: String::new(),
276                hash: String::new(),
277                required: false,
278            },
279            performance: PerformanceSettings {
280                view_distance: 10,
281                simulation_distance: 10,
282                entity_range: 100,
283                sync_chunk_writes: true,
284                max_tick_time: 60000,
285                min_ram: "2G".into(),
286                max_ram: "4G".into(),
287                jvm_args: vec![
288                    "-XX:+UseG1GC".into(),
289                    "-Dsun.rmi.dgc.server.gcInterval=2147483646".into(),
290                ],
291            },
292            web: WebSettings {
293                enabled: false,
294                bind: "0.0.0.0:8080".into(),
295            },
296        }
297    }
298}