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}