1use serde::Deserialize;
2use std::path::PathBuf;
3use anyhow::{Result, Context};
4use log::warn;
5
6#[derive(Debug, Deserialize, Clone)]
7pub struct Config {
8 pub rcon: RconConfig,
9 pub ai: Option<AiConfig>,
10 pub ollama: Option<OllamaConfig>,
11 #[serde(default)]
12 pub server: ServerConfig,
13 #[serde(default)]
14 pub backup: BackupConfig,
15 #[serde(default)]
16 pub notification: NotificationConfig,
17 #[serde(default)]
18 #[allow(dead_code)]
19 pub jvm: JvmConfig,
20}
21
22#[derive(Debug, Deserialize, Clone)]
23pub struct RconConfig {
24 #[serde(default = "default_rcon_host")]
25 pub host: String,
26 #[serde(default = "default_rcon_port")]
27 pub port: u16,
28 pub password: String,
29}
30
31fn default_rcon_host() -> String { "127.0.0.1".to_string() }
32fn default_rcon_port() -> u16 { 25575 }
33
34#[derive(Debug, Deserialize, Clone)]
35pub struct ServerConfig {
36 #[serde(default = "default_jar")]
37 pub jar: String,
38 #[serde(default = "default_min_mem")]
39 pub min_mem: String,
40 #[serde(default = "default_max_mem")]
41 pub max_mem: String,
42 #[serde(default = "default_session_name")]
43 pub session_name: String,
44 #[serde(default = "default_log_file")]
45 pub log_file: String,
46}
47
48fn default_jar() -> String { "fabric-server.jar".to_string() }
49fn default_min_mem() -> String { "512M".to_string() }
50fn default_max_mem() -> String { "1G".to_string() }
51fn default_session_name() -> String { "mc_server".to_string() }
52fn default_log_file() -> String { "logs/latest.log".to_string() }
53
54impl Default for ServerConfig {
55 fn default() -> Self {
56 Self {
57 jar: default_jar(),
58 min_mem: default_min_mem(),
59 max_mem: default_max_mem(),
60 session_name: default_session_name(),
61 log_file: default_log_file(),
62 }
63 }
64}
65
66#[derive(Debug, Deserialize, Clone)]
67pub struct AiConfig {
68 pub api_url: String,
69 pub api_key: String,
70 #[serde(default = "default_model")]
71 pub model: String,
72 #[serde(default = "default_trigger")]
73 pub trigger: String,
74 #[serde(default = "default_max_tokens")]
75 pub max_tokens: u32,
76 #[serde(default = "default_temperature")]
77 pub temperature: f32,
78}
79
80fn default_model() -> String { "gpt-3.5-turbo".to_string() }
81fn default_trigger() -> String { "!".to_string() }
82fn default_max_tokens() -> u32 { 150 }
83fn default_temperature() -> f32 { 0.7 }
84
85#[derive(Debug, Deserialize, Clone)]
86pub struct OllamaConfig {
87 #[serde(default = "default_ollama_enabled")]
88 pub enabled: bool,
89 #[serde(default = "default_ollama_url")]
90 pub url: String,
91 #[serde(default = "default_ollama_model")]
92 pub model: String,
93}
94
95fn default_ollama_enabled() -> bool { false }
96fn default_ollama_url() -> String { "http://localhost:11434/api/generate".to_string() }
97fn default_ollama_model() -> String { "qwen:0.5b".to_string() }
98
99#[derive(Debug, Deserialize, Clone)]
100pub struct BackupConfig {
101 #[serde(default = "default_world_dir")]
102 pub world_dir: String,
103 #[serde(default = "default_backup_dest")]
104 pub backup_dest: String,
105 #[serde(default = "default_retain_days")]
106 pub retain_days: u32,
107}
108
109fn default_world_dir() -> String { "world".to_string() }
110fn default_backup_dest() -> String { "../backups".to_string() }
111fn default_retain_days() -> u32 { 7 }
112
113impl Default for BackupConfig {
114 fn default() -> Self {
115 Self {
116 world_dir: default_world_dir(),
117 backup_dest: default_backup_dest(),
118 retain_days: default_retain_days(),
119 }
120 }
121}
122
123#[derive(Debug, Deserialize, Clone)]
124#[allow(dead_code)]
125pub struct NotificationConfig {
126 #[serde(default)]
127 pub telegram_bot_token: String,
128 #[serde(default)]
129 pub telegram_chat_id: String,
130 #[serde(default = "default_termux_notify")]
131 pub termux_notify: bool,
132}
133
134fn default_termux_notify() -> bool { true }
135
136impl Default for NotificationConfig {
137 fn default() -> Self {
138 Self {
139 telegram_bot_token: String::new(),
140 telegram_chat_id: String::new(),
141 termux_notify: default_termux_notify(),
142 }
143 }
144}
145
146#[derive(Debug, Deserialize, Clone)]
147#[allow(dead_code)]
148pub struct JvmConfig {
149 #[serde(default = "default_gc")]
150 pub gc: String,
151 #[serde(default)]
152 pub extra_flags: String,
153 #[serde(default)]
154 pub xmx: Option<String>,
155 #[serde(default)]
156 pub xms: Option<String>,
157 #[serde(default)]
158 pub jdk_path: Option<String>,
159}
160
161fn default_gc() -> String { "G1GC".to_string() }
162
163impl Default for JvmConfig {
164 fn default() -> Self {
165 Self {
166 gc: default_gc(),
167 extra_flags: String::new(),
168 xmx: None,
169 xms: None,
170 jdk_path: None,
171 }
172 }
173}
174
175impl Config {
176 pub fn load(path: &PathBuf) -> Result<Self> {
177 let content = std::fs::read_to_string(path)
178 .with_context(|| format!("Failed to read config file: {:?}", path))?;
179
180 Self::load_from_str(&content)
181 }
182
183 pub fn load_from_str(content: &str) -> Result<Self> {
184 let mut config: Config = toml::from_str(content)
185 .with_context(|| "Failed to parse config file")?;
186
187 if let Some(ref ai) = config.ai {
189 let using_ollama = config.ollama.as_ref().map(|o| o.enabled).unwrap_or(false);
190
191 if !using_ollama && (ai.api_key.is_empty() || ai.api_url.is_empty()) {
193 warn!("AI configuration incomplete (api_key or api_url is empty) and Ollama is not enabled. AI features will be disabled.");
194 config.ai = None;
195 }
196 }
197
198 Ok(config)
199 }
200
201 pub fn generate_template() -> String {
202 let s = "# MC-Minder Configuration File\n[jvm]\ngc = \"G1GC\"\nextra_flags = \"\"\njdk_path = \"\" # Optional: Custom JDK path\n# xmx = \"2G\" # Uncomment to override server.max_mem\n# xms = \"512M\" # Uncomment to override server.min_mem\n";
204 s.to_string()
205 }
206}