use figment::Figment;
use figment::providers::{Env, Format, Toml};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::core::RunOrder;
const CONFIG_FILENAME: &str = "config.toml";
const APP_NAME: &str = "belt";
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Failed to load configuration: {0}")]
LoadError(String),
#[error("Configuration file not found: {0}")]
NotFound(PathBuf),
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct GlobalConfig {
pub factorio_path: Option<PathBuf>,
#[serde(default)]
pub verbose: bool,
}
impl GlobalConfig {
pub fn from_figment(figment: &Figment) -> Result<Self, ConfigError> {
figment
.extract_inner("global")
.or_else(|_| figment.extract())
.map_err(|e| ConfigError::LoadError(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzeConfig {
#[serde(default)]
pub data_dir: PathBuf,
#[serde(default)]
pub smooth_window: u32,
#[serde(default)]
pub verbose_metrics: Vec<String>,
#[serde(default = "default_height")]
pub height: u32,
#[serde(default = "default_width")]
pub width: u32,
#[serde(default)]
pub max_points: Option<usize>,
}
impl Default for AnalyzeConfig {
fn default() -> Self {
Self {
data_dir: PathBuf::new(),
smooth_window: 0,
verbose_metrics: Vec::new(),
height: default_height(),
width: default_width(),
max_points: None,
}
}
}
fn default_height() -> u32 {
800
}
fn default_width() -> u32 {
1200
}
impl AnalyzeConfig {
pub fn from_figment(figment: &Figment) -> Result<Self, ConfigError> {
figment
.extract_inner("analyze")
.or_else(|_| figment.extract())
.map_err(|e| ConfigError::LoadError(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkConfig {
#[serde(default)]
pub saves_dir: PathBuf,
#[serde(default = "default_ticks")]
pub ticks: u32,
#[serde(default = "default_runs")]
pub runs: u32,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default)]
pub output: Option<PathBuf>,
#[serde(default)]
pub template_path: Option<PathBuf>,
#[serde(default)]
pub mods_dir: Option<PathBuf>,
#[serde(default)]
pub run_order: RunOrder,
#[serde(default)]
pub verbose_metrics: Vec<String>,
#[serde(default)]
pub strip_prefix: Option<String>,
#[serde(default)]
pub headless: Option<bool>,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
saves_dir: PathBuf::new(),
ticks: default_ticks(),
runs: default_runs(),
pattern: None,
output: None,
template_path: None,
mods_dir: None,
run_order: RunOrder::default(),
verbose_metrics: Vec::new(),
strip_prefix: None,
headless: None,
}
}
}
fn default_ticks() -> u32 {
6000
}
fn default_runs() -> u32 {
5
}
impl BenchmarkConfig {
pub fn from_figment(figment: &Figment) -> Result<Self, ConfigError> {
figment
.extract_inner("benchmark")
.or_else(|_| figment.extract())
.map_err(|e| ConfigError::LoadError(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SanitizeConfig {
#[serde(default)]
pub saves_dir: PathBuf,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default = "default_sanitize_ticks")]
pub ticks: u32,
#[serde(default)]
pub mods_dir: Option<PathBuf>,
#[serde(default)]
pub data_dir: Option<PathBuf>,
#[serde(default)]
pub items: Option<String>,
#[serde(default)]
pub fluids: Option<String>,
#[serde(default)]
pub headless: Option<bool>,
}
fn default_sanitize_ticks() -> u32 {
3600
}
impl Default for SanitizeConfig {
fn default() -> Self {
Self {
saves_dir: PathBuf::new(),
pattern: None,
ticks: default_sanitize_ticks(),
mods_dir: None,
data_dir: None,
items: None,
fluids: None,
headless: None,
}
}
}
impl SanitizeConfig {
pub fn from_figment(figment: &Figment) -> Result<Self, ConfigError> {
figment
.extract_inner("sanitize")
.or_else(|_| figment.extract())
.map_err(|e| ConfigError::LoadError(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlueprintConfig {
#[serde(default)]
pub blueprints_dir: PathBuf,
#[serde(default)]
pub base_save_path: PathBuf,
#[serde(default)]
pub count: u32,
#[serde(default)]
pub buffer_ticks: u32,
#[serde(default)]
pub mods_dir: Option<PathBuf>,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default)]
pub output: Option<PathBuf>,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub headless: Option<bool>,
#[serde(default)]
pub bot_count: Option<u32>,
}
impl Default for BlueprintConfig {
fn default() -> Self {
Self {
blueprints_dir: PathBuf::new(),
base_save_path: PathBuf::new(),
count: 0,
buffer_ticks: 0,
mods_dir: None,
pattern: None,
output: None,
prefix: None,
headless: None,
bot_count: None,
}
}
}
impl BlueprintConfig {
pub fn from_figment(figment: &Figment) -> Result<Self, ConfigError> {
figment
.extract_inner("blueprint")
.or_else(|_| figment.extract())
.map_err(|e| ConfigError::LoadError(e.to_string()))
}
}
fn get_config_dir() -> Option<PathBuf> {
dirs::config_dir().map(|dir| dir.join(APP_NAME))
}
fn get_config_file_path() -> Option<PathBuf> {
if let Ok(config_path) = std::env::var("BELT_CONFIG") {
return Some(PathBuf::from(config_path));
}
get_config_dir().map(|dir| dir.join(CONFIG_FILENAME))
}
pub fn create_figment() -> Result<Figment, ConfigError> {
let mut figment = Figment::new();
if let Some(config_path) = get_config_file_path()
&& config_path.exists()
{
figment = figment.merge(Toml::file(config_path));
}
figment = figment.merge(Env::prefixed("BELT_").split("__"));
Ok(figment)
}
pub fn create_figment_from_file(path: &PathBuf) -> Result<Figment, ConfigError> {
if !path.exists() {
return Err(ConfigError::NotFound(path.clone()));
}
let figment = Figment::new()
.merge(Toml::file(path))
.merge(Env::prefixed("BELT_").split("__"));
Ok(figment)
}
pub fn init_config_dir() -> Result<PathBuf, ConfigError> {
let config_dir = dirs::config_dir()
.ok_or_else(|| ConfigError::LoadError("Could not find config directory".to_string()))?;
let belt_config_dir = config_dir.join(APP_NAME);
let config_file = belt_config_dir.join(CONFIG_FILENAME);
if !belt_config_dir.exists() {
std::fs::create_dir_all(&belt_config_dir)
.map_err(|e| ConfigError::LoadError(e.to_string()))?;
}
if !config_file.exists() {
let example_config = r#"# BELT Configuration File
# Place this file at ~/.config/belt/config.toml (Linux/macOS)
# or %APPDATA%\belt\config.toml (Windows)
# Or set BELT_CONFIG environment variable to point to your config file
[global]
# Path to Factorio executable
# factorio_path = "/opt/factorio/bin/factorio"
# verbose = false
[benchmark]
# ticks = 6000
# runs = 5
# run_order = "grouped" # Options: "sequential", "random", "grouped"
# pattern = "*.zip"
# headless = true
[analyze]
# smooth_window = 0
# height = 800
# width = 1200
[sanitize]
# ticks = 3600
# headless = true
[blueprint]
# count = 10
# buffer_ticks = 120
"#;
std::fs::write(&config_file, example_config)
.map_err(|e| ConfigError::LoadError(e.to_string()))?;
}
Ok(config_file)
}