diff --git a/Cargo.lock b/Cargo.lock
index ae74aea..7616a07 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2341,11 +2341,9 @@ dependencies = [
 
 [[package]]
 name = "strafesnet_deferred_loader"
-version = "0.4.1"
+version = "0.5.0"
 dependencies = [
  "strafesnet_common",
- "url",
- "vbsp",
 ]
 
 [[package]]
diff --git a/lib/deferred_loader/Cargo.toml b/lib/deferred_loader/Cargo.toml
index de729c7..0ab812d 100644
--- a/lib/deferred_loader/Cargo.toml
+++ b/lib/deferred_loader/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "strafesnet_deferred_loader"
-version = "0.4.1"
+version = "0.5.0"
 edition = "2021"
 repository = "https://git.itzana.me/StrafesNET/strafe-project"
 license = "MIT OR Apache-2.0"
@@ -9,13 +9,5 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
-[features]
-default = ["legacy"]
-legacy = ["dep:url","dep:vbsp"]
-roblox = []
-source = ["dep:vbsp"]
-
 [dependencies]
 strafesnet_common = { path = "../common", registry = "strafesnet" }
-url = { version = "2.5.2", optional = true }
-vbsp = { version = "0.6.0", optional = true }
diff --git a/lib/deferred_loader/src/deferred_loader.rs b/lib/deferred_loader/src/deferred_loader.rs
new file mode 100644
index 0000000..345e4aa
--- /dev/null
+++ b/lib/deferred_loader/src/deferred_loader.rs
@@ -0,0 +1,109 @@
+use std::collections::HashMap;
+use crate::loader::Loader;
+use crate::mesh::Meshes;
+use crate::texture::{RenderConfigs,Texture};
+use strafesnet_common::model::{Mesh,MeshId,RenderConfig,RenderConfigId,TextureId};
+
+pub enum LoadFailureMode{
+	DefaultToNone,
+	Fatal,
+}
+
+pub struct RenderConfigDeferredLoader<H>{
+	texture_count:u32,
+	render_configs:Vec<RenderConfig>,
+	render_config_id_from_asset_id:HashMap<Option<H>,RenderConfigId>,
+}
+impl<H> RenderConfigDeferredLoader<H>{
+	pub fn new()->Self{
+		Self{
+			texture_count:0,
+			render_configs:Vec::new(),
+			render_config_id_from_asset_id:HashMap::new(),
+		}
+	}
+}
+
+impl<H:core::hash::Hash+Eq> RenderConfigDeferredLoader<H>{
+	pub fn acquire_render_config_id(&mut self,index:Option<H>)->RenderConfigId{
+		let some_texture=index.is_some();
+		*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
+			//create the render config.
+			let render_config=if some_texture{
+				let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
+				self.texture_count+=1;
+				render_config
+			}else{
+				RenderConfig::default()
+			};
+			let render_id=RenderConfigId::new(self.render_configs.len() as u32);
+			self.render_configs.push(render_config);
+			render_id
+		})
+	}
+	pub fn into_render_configs<L:Loader<Index=H,Resource=Texture>>(mut self,loader:&mut L,failure_mode:LoadFailureMode)->Result<RenderConfigs,L::Error>{
+		let mut sorted_textures=vec![None;self.texture_count as usize];
+		for (index_option,render_config_id) in self.render_config_id_from_asset_id{
+			let render_config=&mut self.render_configs[render_config_id.get() as usize];
+			if let (Some(index),Some(texture_id))=(index_option,render_config.texture){
+				let resource_result=loader.load(index);
+				let texture=match failure_mode{
+					// if texture fails to load, use no texture
+					LoadFailureMode::DefaultToNone=>match resource_result{
+						Ok(texture)=>Some(texture),
+						Err(e)=>{
+							render_config.texture=None;
+							println!("Error loading texture: {e}");
+							None
+						},
+					},
+					// loading failure is fatal
+					LoadFailureMode::Fatal=>Some(resource_result?)
+				};
+				sorted_textures[texture_id.get() as usize]=texture;
+			}
+		}
+		Ok(RenderConfigs::new(
+			sorted_textures,
+			self.render_configs,
+		))
+	}
+}
+
+pub struct MeshDeferredLoader<H>{
+	mesh_id_from_asset_id:HashMap<H,MeshId>,
+}
+impl<H> MeshDeferredLoader<H>{
+	pub fn new()->Self{
+		Self{
+			mesh_id_from_asset_id:HashMap::new(),
+		}
+	}
+}
+
+impl<H:core::hash::Hash+Eq> MeshDeferredLoader<H>{
+	pub fn acquire_mesh_id(&mut self,index:H)->MeshId{
+		let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
+		*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
+	}
+	pub fn into_meshes<L:Loader<Index=H,Resource=Mesh>>(self,loader:&mut L,failure_mode:LoadFailureMode)->Result<Meshes,L::Error>{
+		let mut mesh_list=vec![None;self.mesh_id_from_asset_id.len()];
+		for (index,mesh_id) in self.mesh_id_from_asset_id{
+			let resource_result=loader.load(index);
+			let mesh=match failure_mode{
+				// if mesh fails to load, use no mesh
+				LoadFailureMode::DefaultToNone=>match resource_result{
+					Ok(mesh)=>Some(mesh),
+					Err(e)=>{
+						println!("Error loading mesh: {e}");
+						None
+					},
+				},
+				// loading failure is fatal
+				LoadFailureMode::Fatal=>Some(resource_result?)
+			};
+			mesh_list[mesh_id.get() as usize]=mesh;
+		}
+		Ok(Meshes::new(mesh_list))
+	}
+}
diff --git a/lib/deferred_loader/src/lib.rs b/lib/deferred_loader/src/lib.rs
index 767528a..6510201 100644
--- a/lib/deferred_loader/src/lib.rs
+++ b/lib/deferred_loader/src/lib.rs
@@ -1,34 +1,5 @@
-#[cfg(feature="legacy")]
-mod roblox_legacy;
-#[cfg(feature="legacy")]
-mod source_legacy;
-#[cfg(feature="roblox")]
-mod roblox;
-#[cfg(feature="source")]
-mod source;
-
-#[cfg(any(feature="roblox",feature="legacy"))]
-pub mod rbxassetid;
 
+pub mod mesh;
+pub mod loader;
 pub mod texture;
-#[cfg(any(feature="source",feature="legacy"))]
-pub mod valve_mesh;
-#[cfg(any(feature="roblox",feature="legacy"))]
-pub mod roblox_mesh;
-
-#[cfg(feature="legacy")]
-pub fn roblox_legacy()->roblox_legacy::Loader{
-	roblox_legacy::Loader::new()
-}
-#[cfg(feature="legacy")]
-pub fn source_legacy()->source_legacy::Loader{
-	source_legacy::Loader::new()
-}
-#[cfg(feature="roblox")]
-pub fn roblox()->roblox::Loader{
-	roblox::Loader::new()
-}
-#[cfg(feature="source")]
-pub fn source()->source::Loader{
-	source::Loader::new()
-}
+pub mod deferred_loader;
diff --git a/lib/deferred_loader/src/loader.rs b/lib/deferred_loader/src/loader.rs
new file mode 100644
index 0000000..adc9710
--- /dev/null
+++ b/lib/deferred_loader/src/loader.rs
@@ -0,0 +1,8 @@
+use std::error::Error;
+
+pub trait Loader{
+	type Error:Error;
+	type Index;
+	type Resource;
+	fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>;
+}
diff --git a/lib/deferred_loader/src/mesh.rs b/lib/deferred_loader/src/mesh.rs
new file mode 100644
index 0000000..d6a0e5f
--- /dev/null
+++ b/lib/deferred_loader/src/mesh.rs
@@ -0,0 +1,17 @@
+use strafesnet_common::model::{Mesh,MeshId};
+
+pub struct Meshes{
+	meshes:Vec<Option<Mesh>>,
+}
+impl Meshes{
+	pub(crate) const fn new(meshes:Vec<Option<Mesh>>)->Self{
+		Self{
+			meshes,
+		}
+	}
+	pub fn consume(self)->impl Iterator<Item=(MeshId,Mesh)>{
+		self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
+			maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
+		)
+	}
+}
diff --git a/lib/deferred_loader/src/roblox.rs b/lib/deferred_loader/src/roblox.rs
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/deferred_loader/src/roblox_legacy.rs b/lib/deferred_loader/src/roblox_legacy.rs
deleted file mode 100644
index a6480fe..0000000
--- a/lib/deferred_loader/src/roblox_legacy.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-use std::io::Read;
-use std::collections::HashMap;
-use crate::roblox_mesh;
-use crate::texture::{RenderConfigs,Texture};
-use strafesnet_common::model::{MeshId,RenderConfig,RenderConfigId,TextureId};
-use crate::rbxassetid::RobloxAssetId;
-
-#[derive(Default)]
-pub struct RenderConfigLoader{
-	texture_count:u32,
-	render_configs:Vec<RenderConfig>,
-	render_config_id_from_asset_id:HashMap<Option<RobloxAssetId>,RenderConfigId>,
-}
-
-impl RenderConfigLoader{
-	pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
-		let render_id=RenderConfigId::new(self.render_config_id_from_asset_id.len() as u32);
-		let index=name.and_then(|name|{
-			match name.parse::<RobloxAssetId>(){
-				Ok(asset_id)=>Some(asset_id),
-				Err(e)=>{
-					println!("Failed to parse AssetId: {e}");
-					None
-				},
-			}
-		});
-		*self.render_config_id_from_asset_id.entry(index).or_insert_with(||{
-			//create the render config.
-			let render_config=if name.is_some(){
-				let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
-				self.texture_count+=1;
-				render_config
-			}else{
-				RenderConfig::default()
-			};
-			self.render_configs.push(render_config);
-			render_id
-		})
-	}
-}
-
-#[derive(Default)]
-pub struct MeshLoader{
-	mesh_id_from_asset_id:HashMap<Option<RobloxAssetId>,MeshId>,
-}
-
-impl MeshLoader{
-	pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
-		let mesh_id=MeshId::new(self.mesh_id_from_asset_id.len() as u32);
-		let index=match name.parse::<RobloxAssetId>(){
-			Ok(asset_id)=>Some(asset_id),
-			Err(e)=>{
-				println!("Failed to parse AssetId: {e}");
-				None
-			},
-		};
-		*self.mesh_id_from_asset_id.entry(index).or_insert(mesh_id)
-	}
-	pub fn load_meshes(&mut self)->Result<roblox_mesh::Meshes,std::io::Error>{
-		let mut mesh_data=vec![None;self.mesh_id_from_asset_id.len()];
-		for (asset_id_option,mesh_id) in &self.mesh_id_from_asset_id{
-			if let Some(asset_id)=asset_id_option{
-				if let Ok(mut file)=std::fs::File::open(format!("meshes/{}",asset_id.0)){
-					//TODO: parallel
-					let mut data=Vec::<u8>::new();
-					file.read_to_end(&mut data)?;
-					mesh_data[mesh_id.get() as usize]=Some(roblox_mesh::RobloxMeshData::new(data));
-				}else{
-					println!("[roblox_legacy] no mesh name={}",asset_id.0);
-				}
-			}
-		}
-		Ok(roblox_mesh::Meshes::new(mesh_data))
-	}
-}
-
-pub struct Loader{
-	render_config_loader:RenderConfigLoader,
-	mesh_loader:MeshLoader,
-}
-impl Loader{
-	pub fn new()->Self{
-		Self{
-			render_config_loader:RenderConfigLoader::default(),
-			mesh_loader:MeshLoader::default(),
-		}
-	}
-	pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
-		(&mut self.render_config_loader,&mut self.mesh_loader)
-	}
-	pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
-		let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
-		for (asset_id_option,render_config_id) in self.render_config_loader.render_config_id_from_asset_id{
-			let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
-			if let (Some(asset_id),Some(texture_id))=(asset_id_option,render_config.texture){
-				if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",asset_id.0)){
-					//TODO: parallel
-					let mut data=Vec::<u8>::new();
-					file.read_to_end(&mut data)?;
-					sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
-				}else{
-					//texture failed to load
-					render_config.texture=None;
-				}
-			}
-		}
-		Ok(RenderConfigs::new(
-			sorted_textures,
-			self.render_config_loader.render_configs,
-		))
-	}
-}
diff --git a/lib/deferred_loader/src/roblox_mesh.rs b/lib/deferred_loader/src/roblox_mesh.rs
deleted file mode 100644
index cdb545f..0000000
--- a/lib/deferred_loader/src/roblox_mesh.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use strafesnet_common::model::MeshId;
-
-#[derive(Clone)]
-pub struct RobloxMeshData(Vec<u8>);
-impl RobloxMeshData{
-	pub(crate) fn new(data:Vec<u8>)->Self{
-		Self(data)
-	}
-	pub fn get(self)->Vec<u8>{
-		self.0
-	}
-}
-pub struct Meshes{
-	meshes:Vec<Option<RobloxMeshData>>,
-}
-impl Meshes{
-	pub(crate) const fn new(meshes:Vec<Option<RobloxMeshData>>)->Self{
-		Self{
-			meshes,
-		}
-	}
-	pub fn get_texture(&self,texture_id:MeshId)->Option<&RobloxMeshData>{
-		self.meshes.get(texture_id.get() as usize)?.as_ref()
-	}
-	pub fn into_iter(self)->impl Iterator<Item=(MeshId,RobloxMeshData)>{
-		self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
-			maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
-		)
-	}
-}
\ No newline at end of file
diff --git a/lib/deferred_loader/src/source_legacy.rs b/lib/deferred_loader/src/source_legacy.rs
deleted file mode 100644
index fc1474e..0000000
--- a/lib/deferred_loader/src/source_legacy.rs
+++ /dev/null
@@ -1,102 +0,0 @@
-use std::io::Read;
-use std::collections::HashMap;
-use crate::valve_mesh;
-use crate::texture::{Texture,RenderConfigs};
-use strafesnet_common::model::{MeshId,TextureId,RenderConfig,RenderConfigId};
-
-pub struct RenderConfigLoader{
-	texture_count:u32,
-	render_configs:Vec<RenderConfig>,
-	texture_paths:HashMap<Option<Box<str>>,RenderConfigId>,
-}
-impl RenderConfigLoader{
-	pub fn acquire_render_config_id(&mut self,name:Option<&str>)->RenderConfigId{
-		let render_id=RenderConfigId::new(self.texture_paths.len() as u32);
-		*self.texture_paths.entry(name.map(Into::into)).or_insert_with(||{
-			//create the render config.
-			let render_config=if name.is_some(){
-				let render_config=RenderConfig::texture(TextureId::new(self.texture_count));
-				self.texture_count+=1;
-				render_config
-			}else{
-				RenderConfig::default()
-			};
-			self.render_configs.push(render_config);
-			render_id
-		})
-	}
-}
-pub struct MeshLoader{
-	mesh_paths:HashMap<Box<str>,MeshId>,
-}
-impl MeshLoader{
-	pub fn acquire_mesh_id(&mut self,name:&str)->MeshId{
-		let mesh_id=MeshId::new(self.mesh_paths.len() as u32);
-		*self.mesh_paths.entry(name.into()).or_insert(mesh_id)
-	}
-	//load_meshes should look like load_textures
-	pub fn load_meshes(&mut self,bsp:&vbsp::Bsp)->valve_mesh::Meshes{
-		let mut mesh_data=vec![None;self.mesh_paths.len()];
-		for (mesh_path,mesh_id) in &self.mesh_paths{
-			let mesh_path_lower=mesh_path.to_lowercase();
-			//.mdl, .vvd, .dx90.vtx
-			let path=std::path::PathBuf::from(mesh_path_lower.as_str());
-			let mut vvd_path=path.clone();
-			let mut vtx_path=path.clone();
-			vvd_path.set_extension("vvd");
-			vtx_path.set_extension("dx90.vtx");
-			match (bsp.pack.get(mesh_path_lower.as_str()),bsp.pack.get(vvd_path.as_os_str().to_str().unwrap()),bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())){
-				(Ok(Some(mdl_file)),Ok(Some(vvd_file)),Ok(Some(vtx_file)))=>{
-					mesh_data[mesh_id.get() as usize]=Some(valve_mesh::ModelData{
-						mdl:valve_mesh::MdlData::new(mdl_file),
-						vtx:valve_mesh::VtxData::new(vtx_file),
-						vvd:valve_mesh::VvdData::new(vvd_file),
-					});
-				},
-				_=>println!("no model name={}",mesh_path),
-			}
-		}
-		valve_mesh::Meshes::new(mesh_data)
-	}
-}
-
-pub struct Loader{
-	render_config_loader:RenderConfigLoader,
-	mesh_loader:MeshLoader,
-}
-impl Loader{
-	pub fn new()->Self{
-		Self{
-			render_config_loader:RenderConfigLoader{
-				texture_count:0,
-				texture_paths:HashMap::new(),
-				render_configs:Vec::new(),
-			},
-			mesh_loader:MeshLoader{mesh_paths:HashMap::new()},
-		}
-	}
-	pub fn get_inner_mut(&mut self)->(&mut RenderConfigLoader,&mut MeshLoader){
-		(&mut self.render_config_loader,&mut self.mesh_loader)
-	}
-	pub fn into_render_configs(mut self)->Result<RenderConfigs,std::io::Error>{
-		let mut sorted_textures=vec![None;self.render_config_loader.texture_count as usize];
-		for (texture_path,render_config_id) in self.render_config_loader.texture_paths{
-			let render_config=self.render_config_loader.render_configs.get_mut(render_config_id.get() as usize).unwrap();
-			if let (Some(texture_path),Some(texture_id))=(texture_path,render_config.texture){
-				if let Ok(mut file)=std::fs::File::open(format!("textures/{}.dds",texture_path)){
-					//TODO: parallel
-					let mut data=Vec::<u8>::new();
-					file.read_to_end(&mut data)?;
-					sorted_textures[texture_id.get() as usize]=Some(Texture::ImageDDS(data));
-				}else{
-					//texture failed to load
-					render_config.texture=None;
-				}
-			}
-		}
-		Ok(RenderConfigs::new(
-			sorted_textures,
-			self.render_config_loader.render_configs,
-		))
-	}
-}
diff --git a/lib/deferred_loader/src/valve_mesh.rs b/lib/deferred_loader/src/valve_mesh.rs
deleted file mode 100644
index 15d7f40..0000000
--- a/lib/deferred_loader/src/valve_mesh.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use strafesnet_common::model::MeshId;
-
-//duplicate this code for now
-#[derive(Clone)]
-pub struct MdlData(Vec<u8>);
-impl MdlData{
-	pub const fn new(value:Vec<u8>)->Self{
-		Self(value)
-	}
-	pub fn get(self)->Vec<u8>{
-		self.0
-	}
-}
-#[derive(Clone)]
-pub struct VtxData(Vec<u8>);
-impl VtxData{
-	pub const fn new(value:Vec<u8>)->Self{
-		Self(value)
-	}
-	pub fn get(self)->Vec<u8>{
-		self.0
-	}
-}
-#[derive(Clone)]
-pub struct VvdData(Vec<u8>);
-impl VvdData{
-	pub const fn new(value:Vec<u8>)->Self{
-		Self(value)
-	}
-	pub fn get(self)->Vec<u8>{
-		self.0
-	}
-}
-
-#[derive(Clone)]
-pub struct ModelData{
-	pub mdl:MdlData,
-	pub vtx:VtxData,
-	pub vvd:VvdData,
-}
-
-//meshes is more prone to failure
-pub struct Meshes{
-	meshes:Vec<Option<ModelData>>,
-}
-impl Meshes{
-	pub(crate) const fn new(meshes:Vec<Option<ModelData>>)->Self{
-		Self{
-			meshes,
-		}
-	}
-	pub fn get_texture(&self,texture_id:MeshId)->Option<&ModelData>{
-		self.meshes.get(texture_id.get() as usize)?.as_ref()
-	}
-	pub fn into_iter(self)->impl Iterator<Item=(MeshId,ModelData)>{
-		self.meshes.into_iter().enumerate().filter_map(|(mesh_id,maybe_mesh)|
-			maybe_mesh.map(|mesh|(MeshId::new(mesh_id as u32),mesh))
-		)
-	}
-}
\ No newline at end of file
diff --git a/strafe-client/Cargo.toml b/strafe-client/Cargo.toml
index 64f87fe..c4663d7 100644
--- a/strafe-client/Cargo.toml
+++ b/strafe-client/Cargo.toml
@@ -21,7 +21,7 @@ parking_lot = "0.12.1"
 pollster = "0.4.0"
 strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
 strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
-strafesnet_deferred_loader = { path = "../lib/deferred_loader", features = ["legacy"], registry = "strafesnet", optional = true }
+strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "strafesnet", optional = true }
 strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
 strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
 strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }