1use crate::tests_game_config::generate_game_config_for_tests;
2use anyhow::Context;
3use serde::{Deserialize, Serialize};
4use std::{num::NonZeroU32, sync::Arc};
5
6#[derive(
7 Debug, Clone, PartialEq, Eq, Deserialize, strum_macros::Display, strum_macros::EnumString,
8)]
9#[serde(rename_all = "lowercase")]
10#[strum(serialize_all = "lowercase")]
11pub enum Environment {
12 Test,
13 Dev,
14 Prestable,
15 Prod,
16}
17
18#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct WebsocketConfig {
20 #[serde(default = "WebsocketConfig::write_queue_size")]
21 pub write_queue_size: usize,
22 #[serde(default = "WebsocketConfig::max_request_per_sec")]
23 pub max_request_per_sec: NonZeroU32,
24}
25
26impl WebsocketConfig {
27 fn write_queue_size() -> usize {
28 100
29 }
30
31 fn max_request_per_sec() -> NonZeroU32 {
32 NonZeroU32::new(20u32).unwrap()
37 }
38}
39
40pub use axum_service_kit::Version;
41
42#[derive(Clone, Debug, Deserialize)]
43pub struct ModerationConfig {
44 pub language_service_enabled: bool,
45 pub min_confidence_to_block: f32,
46}
47
48#[derive(Clone, Debug, Deserialize)]
49pub struct InfraConfig {
50 #[serde(default = "InfraConfig::port_default")]
51 pub port: u16,
52 pub env: Environment,
53 pub ws_config: WebsocketConfig,
54 pub tg_bot_token: String,
55 pub db_pool_config: deadpool_postgres::Config,
56 pub db_use_tls: bool,
57 #[serde(default = "InfraConfig::txn_max_retries_default")]
58 pub txn_max_retries: u16,
59 #[serde(default = "InfraConfig::db_request_referrals_limit")]
60 pub db_request_referrals_limit: u16,
61 #[serde(default = "InfraConfig::db_request_abilities_limit")]
62 pub db_request_abilities_limit: u16,
63 pub hostname: String,
64 pub web_hostname: Option<String>,
65 pub db_sync_period: f64,
66 #[serde(default = "InfraConfig::state_updater_enabled_default")]
67 pub state_updater_enabled: bool,
68 #[serde(default = "InfraConfig::game_tick_enabled_default")]
69 pub game_tick_enabled: bool,
70 pub cron_trigger_period: f64,
71 pub webauthn_rp_id: String,
72 pub webauthn_rp_origin: String,
73 pub allowed_usernames: Vec<String>,
74 pub otlp_http_export_endpoint: String,
75 pub otlp_username: Option<String>,
76 pub otlp_password: Option<String>,
77 #[serde(default = "InfraConfig::otlp_sampling_rate_default")]
78 pub otlp_sampling_rate: f64,
79 pub show_metrics_logs: bool,
80 pub locale_folder_path: String,
81 pub default_locale: String,
82 pub analytics_gcs_logs_enabled: bool,
83 pub analytics_gcs_bucket: Option<String>,
84 pub analytics_gcs_folder: Option<String>,
85 pub analytics_gcs_upload_interval_secs: u64,
86 pub pyroscope_url: Option<String>,
87 pub firebase_project_id: String,
88 pub backend_version: Version,
89 #[serde(default = "InfraConfig::disconnect_log_events_count_default")]
90 pub disconnect_log_events_count: usize,
91 pub android_package_name: String,
92 pub pubsub_project_id: String,
97 pub pubsub_rtdn_subscription: String,
102 #[serde(default)]
109 pub rtdn_push_token: Option<String>,
110 #[serde(default = "InfraConfig::google_play_max_retries_default")]
111 pub google_play_max_retries: u16,
112 #[serde(default = "InfraConfig::google_play_retry_base_ms_default")]
113 pub google_play_retry_base_ms: u64,
114 pub moderation_config: ModerationConfig,
115 #[serde(default)]
122 pub admin_api_key: Option<String>,
123 #[serde(default)]
128 pub auth_jwt_secret: Option<String>,
129 #[serde(default)]
133 pub realms_service_url: Option<String>,
134 #[serde(default)]
137 pub realm_name: Option<String>,
138}
139
140impl InfraConfig {
141 pub const fn port_default() -> u16 {
142 3000
143 }
144
145 pub const fn txn_max_retries_default() -> u16 {
146 5
147 }
148
149 pub const fn db_request_referrals_limit() -> u16 {
150 10
151 }
152
153 pub const fn db_request_abilities_limit() -> u16 {
154 120
155 }
156
157 pub const fn google_play_max_retries_default() -> u16 {
158 3
159 }
160
161 pub const fn google_play_retry_base_ms_default() -> u64 {
162 500
163 }
164
165 pub const fn disconnect_log_events_count_default() -> usize {
166 20
167 }
168
169 pub fn otlp_sampling_rate_default() -> f64 {
170 1.0
171 }
172
173 pub const fn state_updater_enabled_default() -> bool {
174 true
175 }
176
177 pub const fn game_tick_enabled_default() -> bool {
178 true
179 }
180}
181
182#[derive(Clone, Debug, Deserialize)]
183pub struct Config {
184 pub infra_config: InfraConfig,
185 #[serde(deserialize_with = "deserialize_game_config_arc")]
186 pub game_config: Arc<crate::game_config::GameConfig>,
187 #[serde(skip)]
188 pub config_path: std::path::PathBuf,
189}
190
191fn deserialize_game_config_arc<'de, D>(
192 deserializer: D,
193) -> Result<Arc<crate::game_config::GameConfig>, D::Error>
194where
195 D: serde::Deserializer<'de>,
196{
197 let game_config = crate::game_config::GameConfig::deserialize(deserializer)?;
198 Ok(Arc::new(game_config))
199}
200
201impl Config {
202 pub fn load(
203 path: impl AsRef<std::path::Path> + Send + Sync + Clone + 'static,
204 port: Option<u16>,
205 ) -> anyhow::Result<Self> {
206 let config_path = path.as_ref().to_path_buf();
207 let mut config = schema_kit::parse_from_file::<Self>(path)?;
208
209 config.game_config.validate();
210 config.config_path = config_path;
211
212 if let Some(port) = port {
213 config.infra_config.port = port;
214 }
215
216 if let Ok(v) = std::env::var("ENVIRONMENT") {
217 config.infra_config.env = v
218 .parse::<Environment>()
219 .map_err(|e| anyhow::anyhow!("Invalid ENVIRONMENT value `{v}`: {e}"))?;
220 }
221
222 if let Ok(v) = std::env::var("DB_URL") {
227 config.infra_config.db_pool_config.url = Some(v);
228 }
229
230 if let Ok(v) = std::env::var("DB_HOST") {
231 config.infra_config.db_pool_config.host = Some(v);
232 }
233
234 if let Ok(v) = std::env::var("DB_PORT") {
235 let parsed = v
236 .parse::<u16>()
237 .with_context(|| format!("`{v}` is not a valid u16 for DB_PORT"))?;
238 config.infra_config.db_pool_config.port = Some(parsed);
239 }
240
241 if let Ok(v) = std::env::var("DB_NAME") {
242 config.infra_config.db_pool_config.dbname = Some(v);
243 }
244 if let Ok(v) = std::env::var("DB_USER") {
245 config.infra_config.db_pool_config.user = Some(v);
246 }
247 if let Ok(v) = std::env::var("DB_PASSWORD") {
248 config.infra_config.db_pool_config.password = Some(v);
249 }
250
251 if let Ok(v) = std::env::var("FIREBASEE_PROJECT_ID") {
252 config.infra_config.firebase_project_id = v;
253 }
254
255 if let Ok(v) = std::env::var("PUBSUB_PROJECT_ID") {
256 config.infra_config.pubsub_project_id = v;
257 }
258 if let Ok(v) = std::env::var("PUBSUB_RTDN_SUBSCRIPTION") {
259 config.infra_config.pubsub_rtdn_subscription = v;
260 }
261 if let Ok(v) = std::env::var("RTDN_PUSH_TOKEN") {
262 let trimmed = v.trim();
263 config.infra_config.rtdn_push_token = if trimmed.is_empty() {
264 None
265 } else {
266 Some(trimmed.to_string())
267 };
268 }
269 if let Ok(v) = std::env::var("AUTH_JWT_SECRET") {
270 let trimmed = v.trim();
271 config.infra_config.auth_jwt_secret = if trimmed.is_empty() {
272 None
273 } else {
274 Some(trimmed.to_string())
275 };
276 }
277
278 if let Ok(v) = std::env::var("REALMS_SERVICE_URL") {
279 let trimmed = v.trim();
280 config.infra_config.realms_service_url = if trimmed.is_empty() {
281 None
282 } else {
283 Some(trimmed.to_string())
284 };
285 }
286 if let Ok(v) = std::env::var("REALM_NAME") {
287 let trimmed = v.trim();
288 config.infra_config.realm_name = if trimmed.is_empty() {
289 None
290 } else {
291 Some(trimmed.to_string())
292 };
293 }
294
295 if std::env::var_os("ALLOWED_PUBLIC").is_some() {
296 config.infra_config.allowed_usernames.clear();
297 }
298
299 if let Ok(v) = std::env::var("BACKEND_VERSION") {
300 let version: Version = v.parse().map_err(|e: String| {
301 anyhow::anyhow!("Invalid BACKEND_VERSION in ENV `{v}`: {e}")
302 })?;
303 config.infra_config.backend_version = version;
304 }
305
306 if let Ok(v) = std::env::var("GRAFANA_OTLP_ENDPOINT") {
307 config.infra_config.otlp_http_export_endpoint = v;
308 }
309
310 if let Ok(v) = std::env::var("GRAFANA_OTLP_USERNAME") {
311 config.infra_config.otlp_username = Some(v);
312 }
313
314 if let Ok(v) = std::env::var("GRAFANA_OTLP_PASSWORD") {
315 config.infra_config.otlp_password = Some(v);
316 }
317
318 if let Ok(v) = std::env::var("GRAFANA_OTLP_SAMPLING_RATE") {
319 config.infra_config.otlp_sampling_rate = v.parse::<f64>().with_context(|| {
320 format!("`{v}` is not a valid f64 for GRAFANA_OTLP_SAMPLING_RATE")
321 })?;
322 }
323
324 if let Ok(v) = std::env::var("GRAFANA_PYROSCOPE_URL") {
325 config.infra_config.pyroscope_url = Some(v);
326 }
327
328 if let Ok(v) = std::env::var("ANALYTICS_GCS_BUCKET") {
329 let bucket = v.trim();
330 config.infra_config.analytics_gcs_bucket = if bucket.is_empty() {
331 None
332 } else {
333 Some(bucket.to_string())
334 };
335 }
336
337 if let Ok(v) = std::env::var("ANALYTICS_GCS_FOLDER") {
338 let folder = v.trim().trim_matches('/');
339 config.infra_config.analytics_gcs_folder = if folder.is_empty() {
340 None
341 } else {
342 Some(folder.to_string())
343 };
344 }
345
346 if let Ok(v) = std::env::var("ANALYTICS_GCS_LOGS_ENABLED") {
347 config.infra_config.analytics_gcs_logs_enabled =
348 v.parse::<bool>().with_context(|| {
349 format!("`{v}` is not a valid bool for ANALYTICS_GCS_LOGS_ENABLED")
350 })?;
351 }
352
353 if let Ok(v) = std::env::var("ANALYTICS_GCS_UPLOAD_INTERVAL_SECS") {
354 config.infra_config.analytics_gcs_upload_interval_secs =
355 v.parse::<u64>().with_context(|| {
356 format!("`{v}` is not a valid u64 for ANALYTICS_GCS_UPLOAD_INTERVAL_SECS")
357 })?;
358 }
359
360 if let Ok(v) = std::env::var("ADMIN_API_KEY") {
361 config.infra_config.admin_api_key = Some(v);
362 }
363
364 Ok(config)
365 }
366
367 pub fn load_for_testing(
368 path: impl AsRef<std::path::Path> + Send + Sync + Clone + 'static,
369 ) -> anyhow::Result<Self> {
370 let config_path = path.as_ref().to_path_buf();
371 let mut infra_config = schema_kit::parse_from_file::<InfraConfig>(config_path.clone())?;
372
373 if let Ok(url) = std::env::var("OVERLORD_TEST_DB_URL") {
384 infra_config.db_pool_config.url = Some(url);
385 }
386
387 Ok(Config {
388 infra_config,
389 game_config: Arc::new(generate_game_config_for_tests()),
390 config_path,
391 })
392 }
393}