use std::borrow::Cow;
use std::fs::read_dir;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use rootcause::prelude::*;
use vfs::VfsPath;
use vfs::impls::overlay::OverlayFS;
use crate::data::DataFileWithCallback;
use crate::data::Version;
use crate::data::assets_bin_vfs::AssetsBinVfs;
use crate::data::idx;
use crate::data::idx_vfs::IdxVfs;
use crate::data::wrappers::mmap::MmapPkgSource;
use crate::error::GameDataError;
use crate::rpc::entitydefs::EntitySpec;
use crate::rpc::entitydefs::parse_scripts;
pub fn list_available_builds(game_dir: &Path) -> Result<Vec<u32>, GameDataError> {
let bin_dir = game_dir.join("bin");
let mut builds: Vec<u32> = Vec::new();
for entry in read_dir(&bin_dir)? {
let entry = entry?;
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& let Some(build_num) = entry.file_name().to_str().and_then(|name| name.parse::<u32>().ok())
{
builds.push(build_num);
}
}
builds.sort();
Ok(builds)
}
pub fn find_matching_build(game_dir: &Path, replay_version: &Version) -> Result<u32, GameDataError> {
let available_builds = list_available_builds(game_dir)?;
let build = replay_version.build_number().ok_or(GameDataError::BuildUnknown)?;
if available_builds.contains(&build) { Ok(build) } else { Err(GameDataError::BuildNotFound { build }) }
}
pub struct GameResources {
pub specs: Vec<EntitySpec>,
pub vfs: VfsPath,
}
pub fn load_game_resources(game_dir: &Path, replay_version: &Version) -> Result<GameResources, GameDataError> {
let build = find_matching_build(game_dir, replay_version)?;
let idx_dir = game_dir.join("bin").join(build.to_string()).join("idx");
let mut idx_files = Vec::new();
for entry in read_dir(&idx_dir)? {
let entry = entry?;
if entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
let file_data = std::fs::read(entry.path())?;
idx_files.push(idx::parse(&file_data)?);
}
}
let pkgs_path = game_dir.join("res_packages");
if !pkgs_path.exists() {
return Err(GameDataError::ResPackagesNotFound);
}
let pkg_source = MmapPkgSource::new(&pkgs_path);
let idx_vfs = IdxVfs::new(pkg_source, &idx_files);
let vfs = VfsPath::new(idx_vfs);
let specs = {
let vfs_ref = &vfs;
let loader = DataFileWithCallback::new(move |path: &str| {
let file_path = vfs_ref.join(path)?;
let mut data = Vec::new();
file_path.open_file()?.read_to_end(&mut data)?;
Ok(Cow::Owned(data))
});
parse_scripts(&loader)?
};
Ok(GameResources { specs, vfs })
}
pub fn translations_path(game_dir: &Path, build: u32) -> PathBuf {
game_dir.join("bin").join(build.to_string()).join("res/texts/en/LC_MESSAGES/global.mo")
}
pub fn build_game_vfs(game_dir: &Path) -> Result<VfsPath, Report> {
let builds = list_available_builds(game_dir).attach_with(|| format!("game_dir: {}", game_dir.display()))?;
let latest_build =
*builds.last().ok_or_else(|| rootcause::report!("No builds found in {}/bin", game_dir.display()))?;
build_game_vfs_for_build(game_dir, latest_build)
}
pub fn build_game_vfs_for_build(game_dir: &Path, build: u32) -> Result<VfsPath, Report> {
let idx_dir = game_dir.join("bin").join(build.to_string()).join("idx");
if !idx_dir.exists() {
bail!("idx directory not found: {}", idx_dir.display());
}
let mut idx_files = Vec::new();
let mut idx_errors = Vec::new();
for entry in read_dir(&idx_dir).context_with(|| format!("Failed to read idx dir: {}", idx_dir.display()))? {
let entry = entry?;
if entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
let path = entry.path();
let data = std::fs::read(&path).attach_with(|| format!("path: {}", path.display()))?;
match idx::parse(&data) {
Ok(parsed) => idx_files.push(parsed),
Err(e) => {
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string();
let header_hex: String =
data.iter().take(16).map(|b| format!("{b:02x}")).collect::<Vec<_>>().join(" ");
eprintln!("WARN: Failed to parse idx file {filename}: {e} (header: {header_hex})");
idx_errors.push((filename, e));
}
}
}
}
if idx_files.is_empty() && !idx_errors.is_empty() {
let names: Vec<_> = idx_errors.iter().map(|(n, e)| format!("{n}: {e}")).collect();
bail!("All idx files failed to parse for build {build}:\n {}", names.join("\n "));
}
let pkgs_dir = game_dir.join("res_packages");
if !pkgs_dir.exists() {
bail!("res_packages not found: {}", pkgs_dir.display());
}
let pkg_source = MmapPkgSource::new(&pkgs_dir);
let idx_vfs = IdxVfs::new(pkg_source, &idx_files);
let pkg_vfs = VfsPath::new(idx_vfs);
if let Ok(root) = pkg_vfs.read_dir() {
let count = root.count();
if count == 0 && !idx_files.is_empty() {
eprintln!(
"WARN: VFS is empty despite {} idx files parsing successfully for build {build}",
idx_files.len()
);
let mut on_disk = Vec::new();
if let Ok(entries) = read_dir(&pkgs_dir) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str()
&& name.ends_with(".pkg")
{
on_disk.push(name.to_string());
}
}
}
on_disk.sort();
eprintln!(" pkg files on disk ({}):", on_disk.len());
for pkg in &on_disk {
eprintln!(" {pkg}");
}
for (i, idx) in idx_files.iter().enumerate() {
let _fname = idx_errors.iter().find(|(_, _)| false).map(|(n, _)| n.as_str()).unwrap_or("?");
eprintln!(
" idx[{i}]: {} resources, {} file_infos, {} volumes",
idx.resources.len(),
idx.file_infos.len(),
idx.volumes.len()
);
for vol in &idx.volumes {
let exists = on_disk.iter().any(|p| p == &vol.filename);
eprintln!(" volume {}: {} (exists: {})", vol.volume_id, vol.filename, exists);
}
}
}
}
if !idx_errors.is_empty() {
eprintln!(
"WARN: {}/{} idx files failed to parse for build {build}:",
idx_errors.len(),
idx_errors.len() + idx_files.len()
);
for (name, e) in &idx_errors {
eprintln!(" {name}: {e}");
}
}
let mut assets_bin_data = Vec::new();
let assets_loaded = pkg_vfs
.join("content/assets.bin")
.and_then(|p| p.open_file())
.and_then(|mut f| {
f.read_to_end(&mut assets_bin_data)?;
Ok(())
})
.is_ok();
if assets_loaded && let Ok(assets_vfs) = AssetsBinVfs::new(assets_bin_data) {
let assets_layer = VfsPath::new(assets_vfs);
let overlay = OverlayFS::new(&[assets_layer, pkg_vfs]);
return Ok(VfsPath::new(overlay));
}
Ok(pkg_vfs)
}