1pub const LOG_TARGET: &str = module_path!();
31
32pub mod builds;
33pub mod cas;
34pub mod cas_vfs;
35#[cfg(feature = "constants")]
36pub mod constants;
37#[cfg(feature = "download")]
38pub mod download_repo;
39pub mod dump;
40pub mod manifest;
41pub mod registry;
42
43use std::path::Path;
44use std::path::PathBuf;
45
46use wowsunpack::game_data;
47use wowsunpack::vfs::VfsPath;
48use wowsunpack::vfs::impls::physical::PhysicalFS;
49
50pub fn game_data_dir() -> Option<PathBuf> {
55 if let Ok(dir) = std::env::var("WOWS_GAME_DATA") {
56 let path = PathBuf::from(dir);
57 if path.exists() {
58 return Some(path);
59 }
60 }
61
62 let mut dir = std::env::current_dir().ok()?;
64 loop {
65 if dir.join("game_versions.toml").exists() {
66 let data_dir = dir.join("game_data");
67 return Some(data_dir);
68 }
69 if !dir.pop() {
70 return None;
71 }
72 }
73}
74
75pub fn available_builds() -> Vec<u32> {
80 let Some(data_dir) = game_data_dir() else {
81 return Vec::new();
82 };
83 let reg = registry::load_registry(&data_dir.join("versions.toml"));
84 reg.available_builds().into_iter().filter(|build| reg.game_dir_for_build(*build, &data_dir).is_some()).collect()
88}
89
90pub fn game_dir_for_build(build: u32) -> Option<PathBuf> {
95 let data_dir = game_data_dir()?;
96 let reg = registry::load_registry(&data_dir.join("versions.toml"));
97 reg.game_dir_for_build(build, &data_dir)
98}
99
100pub struct Dump {
107 dump_dir: PathBuf,
108 cas: Option<cas_vfs::BuildCas>,
109}
110
111impl Dump {
112 pub fn open(dump_dir: &Path) -> Self {
114 Self { dump_dir: dump_dir.to_path_buf(), cas: cas_vfs::BuildCas::open(dump_dir) }
115 }
116
117 pub fn vfs(&self) -> VfsPath {
119 match &self.cas {
120 Some(cas) => cas.vfs(),
121 None => VfsPath::new(PhysicalFS::new(self.dump_dir.join("vfs"))),
122 }
123 }
124
125 pub fn derived_path(&self, rel: &str) -> Option<PathBuf> {
128 match &self.cas {
129 Some(cas) => cas.derived_path(rel),
130 None => {
131 let path = self.dump_dir.join(rel);
132 path.exists().then_some(path)
133 }
134 }
135 }
136
137 pub fn has_game_files(&self) -> bool {
141 match &self.cas {
142 Some(cas) => cas.metadata().has_file_hashes() || self.dump_dir.join("vfs").is_dir(),
143 None => self.dump_dir.join("vfs").is_dir(),
144 }
145 }
146}
147
148pub fn dump_for_build(build: u32) -> Option<Dump> {
150 Some(Dump::open(&game_dir_for_build(build)?))
151}
152
153pub fn vfs_for_build(build: u32) -> Option<VfsPath> {
159 let game_dir = game_dir_for_build(build)?;
160 let dump = Dump::open(&game_dir);
161 if dump.has_game_files() {
162 return Some(dump.vfs());
163 }
164 game_data::build_game_vfs(&game_dir).ok()
165}
166
167pub fn latest_build() -> Option<(u32, VfsPath)> {
169 let builds = available_builds();
170 let build = *builds.last()?;
171 let vfs = vfs_for_build(build)?;
172 Some((build, vfs))
173}
174
175#[cfg(test)]
176mod dump_tests {
177 use std::io::Read;
178
179 use super::*;
180 use crate::builds::BuildMetadata;
181
182 fn cas_dump(base: &Path, files: &[(&str, &[u8])], derived: &[(&str, &[u8])]) -> PathBuf {
185 let cas_root = base.join("common");
186 let mut meta = BuildMetadata { version: "1.2.3".into(), build: 100, ..Default::default() };
187 for (rel, bytes) in files {
188 meta.files.insert((*rel).to_string(), cas::store(&cas_root, bytes).unwrap());
189 }
190 for (rel, bytes) in derived {
191 meta.derived.insert((*rel).to_string(), cas::store(&cas_root, bytes).unwrap());
192 }
193 let dump_dir = base.join("1.2.3_100");
194 std::fs::create_dir_all(&dump_dir).unwrap();
195 meta.save(&dump_dir.join("metadata.toml")).unwrap();
196 dump_dir
197 }
198
199 fn legacy_dump(base: &Path, files: &[(&str, &[u8])]) -> PathBuf {
201 let dump_dir = base.join("legacy");
202 for (rel, bytes) in files {
203 let path = dump_dir.join("vfs").join(rel);
204 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
205 std::fs::write(path, bytes).unwrap();
206 }
207 dump_dir
208 }
209
210 fn read(vfs: &VfsPath, rel: &str) -> Vec<u8> {
211 let mut data = Vec::new();
212 vfs.join(rel).unwrap().open_file().unwrap().read_to_end(&mut data).unwrap();
213 data
214 }
215
216 #[test]
217 fn cas_dump_reads_game_files_without_a_vfs_tree() {
218 let base = tempfile::tempdir().unwrap();
219 let dump_dir = cas_dump(base.path(), &[("content/GameParams.data", b"params")], &[]);
220
221 let dump = Dump::open(&dump_dir);
222
223 assert!(!dump_dir.join("vfs").exists(), "a CAS dump has no vfs tree to read");
224 assert!(dump.has_game_files());
225 assert_eq!(read(&dump.vfs(), "content/GameParams.data"), b"params");
226 }
227
228 #[test]
229 fn cas_dump_resolves_derived_artifacts_into_the_store() {
230 let base = tempfile::tempdir().unwrap();
231 let dump_dir = cas_dump(base.path(), &[], &[("game_params.rkyv", b"rkyv bytes")]);
232
233 let dump = Dump::open(&dump_dir);
234 let path = dump.derived_path("game_params.rkyv").expect("derived artifact");
235
236 assert_eq!(std::fs::read(path).unwrap(), b"rkyv bytes");
237 assert!(dump.derived_path("absent.rkyv").is_none());
238 }
239
240 #[test]
241 fn legacy_dump_still_reads_from_its_vfs_tree() {
242 let base = tempfile::tempdir().unwrap();
243 let dump_dir = legacy_dump(base.path(), &[("content/GameParams.data", b"params")]);
244 std::fs::write(dump_dir.join("game_params.rkyv"), b"rkyv bytes").unwrap();
245
246 let dump = Dump::open(&dump_dir);
247
248 assert!(dump.has_game_files());
249 assert_eq!(read(&dump.vfs(), "content/GameParams.data"), b"params");
250 assert_eq!(std::fs::read(dump.derived_path("game_params.rkyv").unwrap()).unwrap(), b"rkyv bytes");
251 }
252
253 #[test]
254 fn empty_directory_has_no_game_files() {
255 let base = tempfile::tempdir().unwrap();
256 let dump_dir = base.path().join("nothing");
257 std::fs::create_dir_all(&dump_dir).unwrap();
258
259 let dump = Dump::open(&dump_dir);
260
261 assert!(!dump.has_game_files());
262 assert!(dump.derived_path("game_params.rkyv").is_none());
263 }
264}
265
266#[cfg(test)]
267mod log_target_tests {
268 #[test]
273 fn every_module_in_this_crate_logs_under_the_crate_target() {
274 assert_eq!(super::LOG_TARGET, "wows_data_mgr");
275 assert!(module_path!().starts_with(super::LOG_TARGET), "{}", module_path!());
276 }
277}