initial version

This commit is contained in:
Robin Appelman 2023-12-20 17:01:29 +01:00
commit 9fd30d00ca
9 changed files with 1568 additions and 0 deletions

111
src/lib.rs Normal file
View file

@ -0,0 +1,111 @@
pub mod source;
pub use source::AssetSource;
use std::env::var_os;
use std::fmt::Debug;
use std::path::PathBuf;
use steamlocate::SteamDir;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LoaderError {
#[error("Failed to find tf2 install location")]
Tf2NotFound,
#[error(transparent)]
Io(#[from] std::io::Error),
#[cfg(feature = "zip-lzma")]
#[error(transparent)]
Zip(#[from] zip_lzma::result::ZipError),
#[error("{0}")]
Other(String),
}
pub struct Loader {
sources: Vec<Box<dyn AssetSource>>,
}
impl Loader {
/// Create the loader
pub fn new() -> Result<Self, LoaderError> {
let tf2_dir = tf2_path()?;
let tf_dir = tf2_dir.join("tf");
let hl_dir = tf2_dir.join("hl2");
let download = tf_dir.join("download");
#[cfg(feature = "bsp")]
let vpks = tf_dir
.read_dir()?
.chain(hl_dir.read_dir()?)
.filter_map(|item| item.ok())
.filter_map(|item| Some(item.path().to_str()?.to_string()))
.filter(|path| path.ends_with("dir.vpk"))
.map(|path| vpk::from_path(&path))
.filter_map(|res| res.ok())
.map(|vpk| Box::new(vpk) as Box<dyn AssetSource>);
#[allow(unused_mut)]
let mut sources = vec![
Box::new(tf_dir) as Box<dyn AssetSource>,
Box::new(download),
Box::new(hl_dir),
];
#[cfg(feature = "bsp")]
sources.extend(vpks);
Ok(Loader { sources })
}
pub fn add_source<S: AssetSource + 'static>(&mut self, source: S) {
self.sources.push(Box::new(source))
}
#[tracing::instrument(skip(self))]
pub fn exists(&self, name: &str) -> Result<bool, LoaderError> {
for source in self.sources.iter() {
if source.has(name)? {
return Ok(true);
}
}
Ok(false)
}
#[tracing::instrument(skip(self))]
pub fn load(&self, name: &str) -> Result<Option<Vec<u8>>, LoaderError> {
for source in self.sources.iter() {
if let Some(data) = source.load(name)? {
return Ok(Some(data));
}
}
Ok(None)
}
pub fn find_in_paths(&self, name: &str, paths: &[String]) -> Option<String> {
for path in paths {
let full_path = format!("{}{}", path, name);
if self.exists(&full_path).unwrap_or_default() {
return Some(full_path);
}
}
None
}
}
fn tf2_path() -> Result<PathBuf, LoaderError> {
if let Some(path) = var_os("TF_DIR") {
let path: PathBuf = path.into();
if path.is_dir() {
Ok(path)
} else {
Err(LoaderError::Tf2NotFound)
}
} else {
Ok(SteamDir::locate()
.ok_or(LoaderError::Tf2NotFound)?
.app(&440)
.ok_or(LoaderError::Tf2NotFound)?
.path
.clone())
}
}

70
src/source.rs Normal file
View file

@ -0,0 +1,70 @@
use crate::LoaderError;
use std::fs::read;
use std::io::ErrorKind;
use std::path::PathBuf;
pub trait AssetSource {
fn has(&self, path: &str) -> Result<bool, LoaderError>;
fn load(&self, path: &str) -> Result<Option<Vec<u8>>, LoaderError>;
}
impl AssetSource for PathBuf {
fn has(&self, path: &str) -> Result<bool, LoaderError> {
Ok(self.join(path).exists())
}
fn load(&self, path: &str) -> Result<Option<Vec<u8>>, LoaderError> {
match read(self.join(path)) {
Ok(data) => Ok(Some(data)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
}
#[cfg(feature = "vpk")]
mod vdf {
use super::AssetSource;
use crate::LoaderError;
use vpk::VPK;
impl AssetSource for VPK {
fn has(&self, path: &str) -> Result<bool, LoaderError> {
Ok(self.tree.contains_key(path))
}
fn load(&self, path: &str) -> Result<Option<Vec<u8>>, LoaderError> {
if let Some(entry) = self.tree.get(path) {
Ok(Some(entry.get()?.into()))
} else {
Ok(None)
}
}
}
}
#[cfg(feature = "vbsp")]
mod vbsp {
use super::AssetSource;
use crate::LoaderError;
use vbsp::{BspError, Packfile};
impl AssetSource for Packfile {
fn has(&self, path: &str) -> Result<bool, LoaderError> {
match self.has(path) {
Ok(found) => Ok(found),
Err(BspError::Zip(err)) => Err(err.into()),
Err(e) => Err(LoaderError::Other(e.to_string())), // the error *should* always be a zip error
}
}
fn load(&self, path: &str) -> Result<Option<Vec<u8>>, LoaderError> {
match self.get(path) {
Ok(data) => Ok(data),
Err(BspError::Zip(err)) => Err(err.into()),
Err(e) => Err(LoaderError::Other(e.to_string())), // the error *should* always be a zip error
}
}
}
}