Files
pz-client-to-dedicated-mult…/src/convert.rs
2026-08-08 12:31:04 +02:00

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(())
}