Skip to main content

wows_data_mgr/
dump.rs

1use std::io::Read;
2use std::path::Path;
3use std::sync::Arc;
4
5use indicatif::ProgressBar;
6use indicatif::ProgressStyle;
7use rootcause::prelude::*;
8use wowsunpack::game_data;
9use wowsunpack::game_params::provider::GameMetadataProvider;
10use wowsunpack::game_params::types::GameParamProvider;
11use wowsunpack::game_params::types::Param;
12use wowsunpack::vfs::VfsFileType;
13use wowsunpack::vfs::VfsPath;
14
15/// VFS directories dumped in their entirety. These are small, targeted
16/// directories containing only the icons/fonts/data the renderer needs.
17const REQUIRED_VFS_DIRS: &[&str] = &[
18    "gui/fla/minimap",
19    "gui/battle_hud/markers_minimap",
20    "gui/battle_hud/icon_frag",
21    "gui/battle_hud/markers/capture_point",
22    "gui/battle_hud/markers/building_icons",
23    "gui/consumables",
24    "gui/powerups/drops",
25    "gui/fonts",
26    "gui/data/constants",
27    "gui/ships_silhouettes",
28    "scripts/entity_defs",
29];
30
31/// Individual VFS files required beyond the directory dumps.
32const REQUIRED_VFS_FILES: &[&str] = &["content/GameParams.data", "scripts/entities.xml"];
33
34/// Files to extract per map from `spaces/<map>/`.
35/// The renderer only needs minimap images and map metadata, not geometry/models.
36const MAP_FILES_SPACES: &[&str] = &["minimap.png", "minimap_water.png", "space.settings"];
37
38/// Files to extract per map from `content/gameplay/<map>/`.
39const MAP_FILES_GAMEPLAY: &[&str] = &["space.settings"];
40
41/// Returns the dump directory path for a given version and build.
42pub fn dump_dir(output_base: &Path, version_str: &str, build: u32) -> std::path::PathBuf {
43    output_base.join(format!("{version_str}_{build}"))
44}
45
46/// Check if a valid dump exists for the given version and build.
47/// A dump is considered valid if its `metadata.toml` marker file exists.
48pub fn dump_exists(output_base: &Path, version_str: &str, build: u32) -> bool {
49    dump_dir(output_base, version_str, build).join("metadata.toml").exists()
50}
51
52/// Dump game data required for replay loading and rendering.
53///
54/// When `progress` is `Some`, a CLI progress bar is updated during extraction.
55/// When `None`, the function runs quietly (suitable for background use in a GUI).
56///
57/// If `allow_existing` is true and a complete dump already exists (metadata.toml present),
58/// returns `Ok(())` immediately. Partial dumps are cleaned up and re-created.
59pub fn dump_renderer_data(
60    game_dir: &Path,
61    build: u32,
62    version_str: &str,
63    output_base: &Path,
64    progress: Option<&ProgressBar>,
65    allow_existing: bool,
66) -> Result<(), Report> {
67    let output_dir = dump_dir(output_base, version_str, build);
68    let vfs_dir = output_dir.join("vfs");
69
70    if output_dir.join("metadata.toml").exists() {
71        if allow_existing {
72            return Ok(());
73        }
74        bail!("Output directory already exists: {}", output_dir.display());
75    }
76
77    // Clean up partial dumps
78    if output_dir.exists() {
79        std::fs::remove_dir_all(&output_dir)?;
80    }
81
82    let vfs = game_data::build_game_vfs(game_dir).attach_with(|| "Failed to build game VFS")?;
83
84    // Extract VFS directories
85    for dir in REQUIRED_VFS_DIRS {
86        extract_vfs_dir(&vfs, dir, &vfs_dir, progress)?;
87    }
88    for file in REQUIRED_VFS_FILES {
89        extract_vfs_file(&vfs, file, &vfs_dir)?;
90        if let Some(pb) = progress {
91            pb.inc(1);
92        }
93    }
94    extract_map_files(&vfs, "spaces", MAP_FILES_SPACES, &vfs_dir, progress)?;
95    extract_map_files(&vfs, "content/gameplay", MAP_FILES_GAMEPLAY, &vfs_dir, progress)?;
96
97    if let Some(pb) = progress {
98        pb.finish_and_clear();
99    }
100
101    // Serialize GameParams via rkyv
102    let game_params = GameMetadataProvider::from_vfs(&vfs).map_err(|e| report!("Failed to load GameParams: {e:?}"))?;
103    let params: Vec<Param> = game_params.params().iter().map(|p| Arc::unwrap_or_clone(Arc::clone(p))).collect();
104    let bytes =
105        rkyv::to_bytes::<rkyv::rancor::Error>(&params).map_err(|e| report!("Failed to serialize GameParams: {e}"))?;
106    std::fs::write(output_dir.join("game_params.rkyv"), &bytes).attach_with(|| "Failed to write game_params.rkyv")?;
107
108    // Copy all language translations
109    dump_all_translations(game_dir, build, &output_dir)?;
110
111    // Write metadata last (acts as completion marker)
112    let metadata = format!("version = \"{version_str}\"\nbuild = {build}\n");
113    std::fs::write(output_dir.join("metadata.toml"), metadata)?;
114
115    Ok(())
116}
117
118/// Create a configured progress bar for CLI use. Call before `dump_renderer_data`.
119pub fn create_progress_bar(game_dir: &Path) -> Option<ProgressBar> {
120    let vfs = game_data::build_game_vfs(game_dir).ok()?;
121    let mut total_files = 0u64;
122    for dir in REQUIRED_VFS_DIRS {
123        total_files += count_vfs_dir_files(&vfs, dir);
124    }
125    total_files += REQUIRED_VFS_FILES.len() as u64;
126    total_files += count_map_files(&vfs, "spaces", MAP_FILES_SPACES);
127    total_files += count_map_files(&vfs, "content/gameplay", MAP_FILES_GAMEPLAY);
128
129    let pb = ProgressBar::new(total_files);
130    pb.set_style(
131        ProgressStyle::default_bar()
132            .template("{msg} [{bar:40}] {pos}/{len}")
133            .expect("valid template")
134            .progress_chars("=> "),
135    );
136    pb.set_message("Extracting VFS");
137    Some(pb)
138}
139
140/// Copy all language translation files from the game install to the dump directory.
141fn dump_all_translations(game_dir: &Path, build: u32, output_dir: &Path) -> Result<(), Report> {
142    let texts_dir = game_dir.join("bin").join(build.to_string()).join("res/texts");
143    if !texts_dir.exists() {
144        tracing::warn!("Translations directory not found: {}", texts_dir.display());
145        return Ok(());
146    }
147    for entry in std::fs::read_dir(&texts_dir)?.flatten() {
148        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
149            continue;
150        }
151        let lang = entry.file_name();
152        let mo_src = entry.path().join("LC_MESSAGES/global.mo");
153        if mo_src.exists() {
154            let mo_dest = output_dir.join("translations").join(&lang).join("LC_MESSAGES/global.mo");
155            std::fs::create_dir_all(mo_dest.parent().unwrap())?;
156            std::fs::copy(&mo_src, &mo_dest)?;
157        }
158    }
159    Ok(())
160}
161
162// -- Internal helpers --
163
164fn count_vfs_dir_files(vfs: &VfsPath, dir: &str) -> u64 {
165    let mut count = 0;
166    if let Ok(vfs_dir_path) = vfs.join(dir)
167        && let Ok(walker) = vfs_dir_path.walk_dir()
168    {
169        for entry in walker.flatten() {
170            if entry.metadata().map(|m| m.file_type == VfsFileType::File).unwrap_or(false) {
171                count += 1;
172            }
173        }
174    }
175    count
176}
177
178fn count_map_files(vfs: &VfsPath, parent_dir: &str, filenames: &[&str]) -> u64 {
179    let mut count = 0;
180    if let Ok(parent) = vfs.join(parent_dir)
181        && let Ok(entries) = parent.read_dir()
182    {
183        for entry in entries {
184            if entry.metadata().map(|m| m.file_type == VfsFileType::Directory).unwrap_or(false) {
185                for filename in filenames {
186                    if entry.join(filename).is_ok_and(|f: VfsPath| f.exists().unwrap_or(false)) {
187                        count += 1;
188                    }
189                }
190            }
191        }
192    }
193    count
194}
195
196fn extract_map_files(
197    vfs: &VfsPath,
198    parent_dir: &str,
199    filenames: &[&str],
200    output_root: &Path,
201    progress: Option<&ProgressBar>,
202) -> Result<(), Report> {
203    let parent = match vfs.join(parent_dir) {
204        Ok(d) => d,
205        Err(_) => return Ok(()),
206    };
207    let entries = match parent.read_dir() {
208        Ok(e) => e,
209        Err(_) => return Ok(()),
210    };
211
212    for entry in entries {
213        if !entry.metadata().map(|m| m.file_type == VfsFileType::Directory).unwrap_or(false) {
214            continue;
215        }
216        for filename in filenames {
217            let file_path = match entry.join(filename) {
218                Ok(f) => f,
219                Err(_) => continue,
220            };
221            if !file_path.exists().unwrap_or(false) {
222                continue;
223            }
224            let rel = file_path.as_str();
225            let dest = output_root.join(rel.trim_start_matches('/'));
226            if let Some(parent_path) = dest.parent() {
227                std::fs::create_dir_all(parent_path)?;
228            }
229            let mut src = file_path.open_file().attach_with(|| format!("Failed to open VFS file: {rel}"))?;
230            let mut buf = Vec::new();
231            src.read_to_end(&mut buf)?;
232            std::fs::write(&dest, &buf)?;
233            if let Some(pb) = progress {
234                pb.inc(1);
235            }
236        }
237    }
238    Ok(())
239}
240
241fn extract_vfs_dir(
242    vfs: &VfsPath,
243    vfs_path: &str,
244    output_root: &Path,
245    progress: Option<&ProgressBar>,
246) -> Result<(), Report> {
247    let dir = match vfs.join(vfs_path) {
248        Ok(d) => d,
249        Err(_) => return Ok(()),
250    };
251    let walker = match dir.walk_dir() {
252        Ok(w) => w,
253        Err(_) => return Ok(()),
254    };
255
256    for entry in walker.flatten() {
257        let metadata = match entry.metadata() {
258            Ok(m) => m,
259            Err(_) => continue,
260        };
261        if metadata.file_type != VfsFileType::File {
262            continue;
263        }
264        let rel = entry.as_str();
265        let dest = output_root.join(rel.trim_start_matches('/'));
266        if let Some(parent) = dest.parent() {
267            std::fs::create_dir_all(parent)?;
268        }
269        let mut src = entry.open_file().attach_with(|| format!("Failed to open VFS file: {rel}"))?;
270        let mut buf = Vec::new();
271        src.read_to_end(&mut buf)?;
272        std::fs::write(&dest, &buf)?;
273        if let Some(pb) = progress {
274            pb.inc(1);
275        }
276    }
277    Ok(())
278}
279
280fn extract_vfs_file(vfs: &VfsPath, vfs_path: &str, output_root: &Path) -> Result<(), Report> {
281    let file = vfs.join(vfs_path).attach_with(|| format!("VFS path not found: {vfs_path}"))?;
282    let dest = output_root.join(vfs_path);
283    if let Some(parent) = dest.parent() {
284        std::fs::create_dir_all(parent)?;
285    }
286    let mut src = file.open_file().attach_with(|| format!("Failed to open VFS file: {vfs_path}"))?;
287    let mut buf = Vec::new();
288    src.read_to_end(&mut buf)?;
289    std::fs::write(&dest, &buf)?;
290    Ok(())
291}