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
15const 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
31const REQUIRED_VFS_FILES: &[&str] = &["content/GameParams.data", "scripts/entities.xml"];
33
34const MAP_FILES_SPACES: &[&str] = &["minimap.png", "minimap_water.png", "space.settings"];
37
38const MAP_FILES_GAMEPLAY: &[&str] = &["space.settings"];
40
41pub fn dump_dir(output_base: &Path, version_str: &str, build: u32) -> std::path::PathBuf {
43 output_base.join(format!("{version_str}_{build}"))
44}
45
46pub 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
52pub 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 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 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 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>(¶ms).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 dump_all_translations(game_dir, build, &output_dir)?;
110
111 let metadata = format!("version = \"{version_str}\"\nbuild = {build}\n");
113 std::fs::write(output_dir.join("metadata.toml"), metadata)?;
114
115 Ok(())
116}
117
118pub 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
140fn 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
162fn 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}