205 lines
6.0 KiB
Rust
205 lines
6.0 KiB
Rust
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 \
|
|
<output>/../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(())
|
|
}
|