use crate::copy; use crate::input::ResolvedInput; use crate::mods; use crate::password; use crate::players_db; use anyhow::{bail, Context, Result}; use std::fs; use std::path::Path; pub struct ConvertOptions<'a> { pub input: &'a Path, pub output: &'a Path, pub server_name: &'a str, pub server_config: Option<&'a Path>, pub workshop_dir: Option<&'a Path>, pub password: Option<&'a str>, pub env_file: Option<&'a Path>, pub force: bool, } pub fn convert(opts: &ConvertOptions<'_>) -> Result<()> { let resolved = ResolvedInput::resolve(opts.input)?; let dest_world = opts .output .join("Saves") .join("Multiplayer") .join(opts.server_name); if dest_world.exists() && !opts.force { bail!( "destination world already exists: {} (use --force to replace)", dest_world.display() ); } eprintln!( "Converting {} -> {}", resolved.world_root.display(), dest_world.display() ); let copied = copy::copy_world_filtered(&resolved.world_root, &dest_world)?; eprintln!("Copied {copied} files"); let players_db_path = dest_world.join("players.db"); if players_db_path.is_file() { let updated = players_db::patch_world_column(&players_db_path, opts.server_name)?; eprintln!( "Updated {updated} networkPlayers rows (world -> {})", opts.server_name ); } else { eprintln!("Warning: no players.db found in destination world"); } if let Some(config_dir) = opts.server_config { copy_server_config(config_dir, opts.output, &resolved.world_name, opts.server_name)?; } recover_mods_into_server_ini(opts, &resolved.world_root)?; apply_server_password(opts)?; eprintln!("Done."); Ok(()) } fn apply_server_password(opts: &ConvertOptions<'_>) -> Result<()> { let Some(pw) = password::resolve_password(opts.password, opts.env_file, opts.output)? else { eprintln!( "No server password set (pass --password or SERVER_PASSWORD/PASSWORD in .env); leaving Password= unchanged" ); return Ok(()); }; let ini = opts .output .join("Server") .join(format!("{}.ini", opts.server_name)); if !ini.is_file() { eprintln!( "Warning: {} missing — cannot apply Password=", ini.display() ); return Ok(()); } password::patch_ini_key(&ini, "Password", &pw)?; eprintln!("Patched Password= in {}", ini.display()); Ok(()) } fn recover_mods_into_server_ini(opts: &ConvertOptions<'_>, world_root: &Path) -> Result<()> { let mod_ids = mods::extract_mod_ids(world_root)?; if mod_ids.is_empty() { eprintln!("No mod IDs found in WorldDictionaryReadable.lua"); return Ok(()); } eprintln!("Recovered {} mod ID(s) from WorldDictionary", mod_ids.len()); let Some(workshop_dir) = opts.workshop_dir else { eprintln!( "Warning: no workshop dir (pass --workshop-dir or place mods under \ /../server-files/steamapps/workshop/content/108600); wrote Mods= only" ); let mods_line = mod_ids.join(";"); let ini = opts.output.join("Server").join(format!("{}.ini", opts.server_name)); if ini.is_file() { // Keep existing WorkshopItems if any let text = fs::read_to_string(&ini)?; let workshop = text .lines() .find_map(|l| l.strip_prefix("WorkshopItems=")) .unwrap_or("") .to_string(); mods::patch_server_ini_mods(&ini, &mods_line, &workshop)?; eprintln!("Patched Mods= in {}", ini.display()); } else { eprintln!( "Warning: {} missing — create/start the server once, then re-run with --workshop-dir", ini.display() ); } return Ok(()); }; let (workshop_ids, _) = mods::resolve_workshop_ids(&mod_ids, workshop_dir)?; let mods_line = mod_ids.join(";"); let workshop_line = workshop_ids.join(";"); let server_dir = opts.output.join("Server"); fs::create_dir_all(&server_dir)?; let ini = server_dir.join(format!("{}.ini", opts.server_name)); if !ini.is_file() { bail!( "server ini not found at {} — start the dedicated server once to generate it, then re-run", ini.display() ); } mods::patch_server_ini_mods(&ini, &mods_line, &workshop_line)?; eprintln!( "Patched {} ({} mods, {} workshop items)", ini.display(), mod_ids.len(), workshop_ids.len() ); Ok(()) } fn copy_server_config( config_dir: &Path, output: &Path, old_name: &str, server_name: &str, ) -> Result<()> { let dest_server = output.join("Server"); fs::create_dir_all(&dest_server)?; let mappings = [ (format!("{old_name}.ini"), format!("{server_name}.ini")), ( format!("{old_name}_SandboxVars.lua"), format!("{server_name}_SandboxVars.lua"), ), ( format!("{old_name}_spawnpoints.lua"), format!("{server_name}_spawnpoints.lua"), ), ( format!("{old_name}_spawnregions.lua"), format!("{server_name}_spawnregions.lua"), ), ]; let mut copied_any = false; for (src_name, dest_name) in mappings { let src = config_dir.join(&src_name); if src.is_file() { let dest = dest_server.join(&dest_name); fs::copy(&src, &dest).with_context(|| { format!( "failed to copy server config {} -> {}", src.display(), dest.display() ) })?; eprintln!("Copied server config: {src_name} -> {dest_name}"); copied_any = true; } } if !copied_any { eprintln!( "Warning: no server config files found in {} for world name `{old_name}`", config_dir.display() ); } Ok(()) }