diff --git a/Cargo.lock b/Cargo.lock
index b0aeb45..5373fea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3803,6 +3803,7 @@ dependencies = [
  "rbx_binary",
  "rbx_dom_weak",
  "rbx_mesh",
+ "rbx_reflection",
  "rbx_reflection_database",
  "rbx_xml",
  "rbxassetid 0.1.0",
diff --git a/lib/rbx_loader/Cargo.toml b/lib/rbx_loader/Cargo.toml
index d2415bf..2dfed94 100644
--- a/lib/rbx_loader/Cargo.toml
+++ b/lib/rbx_loader/Cargo.toml
@@ -16,6 +16,7 @@ lazy-regex = "3.1.0"
 rbx_binary  = { version = "1.1.0-sn4", registry = "strafesnet" }
 rbx_dom_weak =  { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
 rbx_mesh = "0.3.1"
+rbx_reflection = "5.0.0"
 rbx_reflection_database = "1.0.0"
 rbx_xml  = { version = "1.1.0-sn4", registry = "strafesnet" }
 rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
diff --git a/lib/rbx_loader/src/error.rs b/lib/rbx_loader/src/error.rs
new file mode 100644
index 0000000..335386c
--- /dev/null
+++ b/lib/rbx_loader/src/error.rs
@@ -0,0 +1,117 @@
+use std::collections::HashSet;
+use std::num::ParseIntError;
+
+use strafesnet_common::gameplay_modes::{StageId,ModeId};
+use strafesnet_common::integer::{FixedFromFloatError,Planar64TryFromFloatError};
+
+/// A collection of errors which can be ignored at your peril
+#[derive(Debug,Default)]
+pub struct RecoverableErrors{
+	/// A basepart has an invalid / missing property.
+	pub basepart_property:Vec<InstancePath>,
+	/// A part has an unconvertable CFrame.
+	pub basepart_cframe:Vec<CFrameError>,
+	/// A part has an unconvertable Velocity.
+	pub basepart_velocity:Vec<Planar64ConvertError>,
+	/// A part has an invalid / missing property.
+	pub part_property:Vec<InstancePath>,
+	/// A part has an invalid shape.
+	pub part_shape:Vec<ShapeError>,
+	/// A meshpart has an invalid / missing property.
+	pub meshpart_property:Vec<InstancePath>,
+	/// A meshpart has no mesh.
+	pub meshpart_content:Vec<InstancePath>,
+	/// A basepart has an unsupported subclass.
+	pub unsupported_class:HashSet<String>,
+	/// A decal has an invalid / missing property.
+	pub decal_property:Vec<InstancePath>,
+	/// A decal has an invalid normal_id.
+	pub normal_id:Vec<NormalIdError>,
+	/// A texture has an invalid / missing property.
+	pub texture_property:Vec<InstancePath>,
+	/// A mode_id failed to parse.
+	pub mode_id_parse_int:Vec<ParseIntContext>,
+	/// There is a duplicate mode.
+	pub duplicate_mode:HashSet<ModeId>,
+	/// A mode_id failed to parse.
+	pub stage_id_parse_int:Vec<ParseIntContext>,
+	/// A Stage was duplicated leading to undefined behaviour.
+	pub duplicate_stage:HashSet<DuplicateStageError>,
+	/// A WormholeOut id failed to parse.
+	pub wormhole_out_id_parse_int:Vec<ParseIntContext>,
+	/// A WormholeOut was duplicated leading to undefined behaviour.
+	pub duplicate_wormhole_out:HashSet<u32>,
+	/// A WormholeIn id failed to parse.
+	pub wormhole_in_id_parse_int:Vec<ParseIntContext>,
+	/// A jump limit failed to parse.
+	pub jump_limit_parse_int:Vec<ParseIntContext>,
+}
+
+/// A Decal was missing required properties
+#[derive(Debug)]
+pub struct InstancePath(pub String);
+
+impl InstancePath{
+	pub fn new(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->InstancePath{
+		let mut names:Vec<_>=core::iter::successors(
+			Some(instance),
+			|i|dom.get_by_ref(i.parent())
+		).map(
+			|i|i.name.as_str()
+		).collect();
+		// discard the name of the root object
+		names.pop();
+		names.reverse();
+		InstancePath(names.join("."))
+	}
+}
+
+#[derive(Debug)]
+pub struct ParseIntContext{
+	pub context:String,
+	pub error:ParseIntError,
+}
+impl ParseIntContext{
+	pub fn parse<T:core::str::FromStr<Err=ParseIntError>>(input:&str)->Result<T,Self>{
+		input.parse().map_err(|error|ParseIntContext{
+			context:input.to_owned(),
+			error,
+		})
+	}
+}
+
+#[derive(Debug)]
+pub struct NormalIdError{
+	pub path:InstancePath,
+	pub normal_id:u32,
+}
+
+#[derive(Debug)]
+pub struct ShapeError{
+	pub path:InstancePath,
+	pub shape:u32,
+}
+
+#[derive(Debug)]
+pub enum CFrameErrorType{
+	ZeroDeterminant,
+	Convert(FixedFromFloatError),
+}
+
+#[derive(Debug)]
+pub struct CFrameError{
+	pub path:InstancePath,
+	pub error:CFrameErrorType,
+}
+
+#[derive(Debug)]
+pub struct Planar64ConvertError{
+	pub path:InstancePath,
+	pub error:Planar64TryFromFloatError,
+}
+
+#[derive(Debug,Hash,Eq,PartialEq)]
+pub struct DuplicateStageError{
+	pub mode_id:ModeId,
+	pub stage_id:StageId,
+}
diff --git a/lib/rbx_loader/src/lib.rs b/lib/rbx_loader/src/lib.rs
index 723f481..3e950d0 100644
--- a/lib/rbx_loader/src/lib.rs
+++ b/lib/rbx_loader/src/lib.rs
@@ -1,10 +1,14 @@
 use std::io::Read;
 use rbx_dom_weak::WeakDom;
 use roblox_emulator::context::Context;
+use strafesnet_common::map::CompleteMap;
 use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
 
+pub use error::RecoverableErrors;
+
 mod rbx;
 mod mesh;
+mod error;
 mod union;
 pub mod loader;
 mod primitives;
@@ -28,7 +32,7 @@ impl Model{
 	fn new(dom:WeakDom)->Self{
 		Self{dom}
 	}
-	pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
+	pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
 		to_snf(self,failure_mode)
 	}
 }
@@ -59,7 +63,7 @@ impl Place{
 			}
 		}
 	}
-	pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
+	pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
 		to_snf(self,failure_mode)
 	}
 }
@@ -123,7 +127,7 @@ impl From<loader::MeshError> for LoadError{
 	}
 }
 
-fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesnet_common::map::CompleteMap,LoadError>{
+fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
 	let dom=dom.as_ref();
 
 	let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
@@ -143,7 +147,5 @@ fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<strafesn
 	let mut texture_loader=loader::TextureLoader::new();
 	let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
 
-	let map=map_step2.add_render_configs_and_textures(render_configs);
-
-	Ok(map)
+	Ok(map_step2.add_render_configs_and_textures(render_configs))
 }
diff --git a/lib/rbx_loader/src/rbx.rs b/lib/rbx_loader/src/rbx.rs
index 8f332a3..01fb76e 100644
--- a/lib/rbx_loader/src/rbx.rs
+++ b/lib/rbx_loader/src/rbx.rs
@@ -1,4 +1,5 @@
 use std::collections::HashMap;
+use crate::error::{RecoverableErrors,CFrameError,CFrameErrorType,DuplicateStageError,InstancePath,NormalIdError,Planar64ConvertError,ParseIntContext,ShapeError};
 use crate::loader::{MeshWithSize,MeshIndex};
 use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
 use strafesnet_common::map;
@@ -6,7 +7,7 @@ use strafesnet_common::model;
 use strafesnet_common::gameplay_modes::{NormalizedModes,Mode,ModeId,ModeUpdate,ModesBuilder,Stage,StageElement,StageElementBehaviour,StageId,Zone};
 use strafesnet_common::gameplay_style;
 use strafesnet_common::gameplay_attributes as attr;
-use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
+use strafesnet_common::integer::{self,vec3,Planar64TryFromFloatError,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
 use strafesnet_common::model::RenderConfigId;
 use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
 use strafesnet_deferred_loader::mesh::Meshes;
@@ -17,40 +18,32 @@ fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
 	rbx_dom_weak::ustr(s)
 }
 
-fn recursive_collect_superclass(
-	objects:&mut std::vec::Vec<rbx_dom_weak::types::Ref>,
-	dom:&rbx_dom_weak::WeakDom,
-	instance:&rbx_dom_weak::Instance,
-	superclass:&str
-){
-	let instance=instance;
-	let db=rbx_reflection_database::get();
-	let Some(superclass)=db.classes.get(superclass)else{
-		return;
-	};
-	objects.extend(
-		dom.descendants_of(instance.referent()).filter_map(|instance|{
-			let class=db.classes.get(instance.class.as_str())?;
-			db.has_superclass(class,superclass).then(||instance.referent())
-		})
-	);
-}
-
-fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Planar64Affine3{
-	Planar64Affine3::new(
+fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Result<Planar64Affine3,Planar64TryFromFloatError>{
+	Ok(Planar64Affine3::new(
 		Planar64Mat3::from_cols([
-			vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x]).unwrap()
-			*integer::try_from_f32(size.x/2.0).unwrap(),
-			vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y]).unwrap()
-			*integer::try_from_f32(size.y/2.0).unwrap(),
-			vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z]).unwrap()
-			*integer::try_from_f32(size.z/2.0).unwrap(),
-			].map(|t|t.narrow_1().unwrap())),
-		vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z]).unwrap()
-	)
+			(vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x])?
+			*integer::try_from_f32(size.x/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
+			(vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y])?
+			*integer::try_from_f32(size.y/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
+			(vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z])?
+			*integer::try_from_f32(size.z/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
+		]),
+		vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z])?
+	))
 }
 
-fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->attr::CollisionAttributes{
+enum GetAttributesError{
+	ModeIdParseInt(ParseIntContext),
+	DuplicateMode(ModeId),
+	StageIdParseInt(ParseIntContext),
+	DuplicateStage(DuplicateStageError),
+	WormholeOutIdParseInt(ParseIntContext),
+	DuplicateWormholeOut(u32),
+	WormholeInIdParseInt(ParseIntContext),
+	JumpLimitParseInt(ParseIntContext),
+}
+
+fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->Result<attr::CollisionAttributes,GetAttributesError>{
 	let mut general=attr::GeneralAttributes::default();
 	let mut intersecting=attr::IntersectingAttributes::default();
 	let mut contacting=attr::ContactingAttributes::default();
@@ -84,13 +77,14 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 		"MapStart"=>{
 			force_can_collide=false;
 			force_intersecting=true;
+			let mode_id=ModeId::MAIN;
 			modes_builder.insert_mode(
-				ModeId::MAIN,
+				mode_id,
 				Mode::empty(
 					gameplay_style::StyleModifiers::roblox_bhop(),
 					model_id
 				)
-			).unwrap();
+			).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
 		},
 		"MapFinish"=>{
 			force_can_collide=false;
@@ -130,26 +124,30 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 					"BonusStart"=>{
 						force_can_collide=false;
 						force_intersecting=true;
+						let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
 						modes_builder.insert_mode(
-							ModeId::new(captures[2].parse::<u32>().unwrap()),
+							mode_id,
 							Mode::empty(
 								gameplay_style::StyleModifiers::roblox_bhop(),
 								model_id
 							)
-						).unwrap();
+						).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
 					},
 					"WormholeOut"=>{
 						//the PhysicsModelId has to exist for it to be teleported to!
 						force_intersecting=true;
 						//this object is not special in strafe client, but the roblox mapping needs to be converted to model id
-						assert!(wormhole_id_to_out_model.insert(captures[2].parse::<u32>().unwrap(),model_id).is_none(),"Cannot have multiple WormholeOut with same id");
+						let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeOutIdParseInt)?;
+						if wormhole_id_to_out_model.insert(wormhole_id,model_id).is_some(){
+							return Err(GetAttributesError::DuplicateWormholeOut(wormhole_id));
+						}
 					},
 					_=>(),
 				}
 			}else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
 			.captures(other){
 				force_intersecting=true;
-				let stage_id=StageId::new(captures[3].parse::<u32>().unwrap());
+				let stage_id=StageId::new(ParseIntContext::parse(&captures[3]).map_err(GetAttributesError::StageIdParseInt)?);
 				let stage_element=StageElement::new(
 					//stage_id:
 					stage_id,
@@ -161,11 +159,12 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 					//behaviour:
 					match &captures[2]{
 						"Spawn"=>{
+							let mode_id=ModeId::MAIN;
 							modes_builder.insert_stage(
-								ModeId::MAIN,
+								mode_id,
 								stage_id,
 								Stage::empty(model_id),
-							).unwrap();
+							).map_err(|_|GetAttributesError::DuplicateStage(DuplicateStageError{mode_id,stage_id}))?;
 							//TODO: let denormalize handle this
 							StageElementBehaviour::SpawnAt
 						},
@@ -175,7 +174,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 						"Trigger"=>{force_can_collide=false;StageElementBehaviour::Trigger},
 						"Teleport"=>{force_can_collide=false;StageElementBehaviour::Teleport},
 						"Platform"=>StageElementBehaviour::Platform,
-						_=>panic!("regex1[2] messed up bad"),
+						_=>unreachable!("regex1[2] messed up bad"),
 					},
 					None
 				);
@@ -198,30 +197,33 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 								StageId::FIRST,
 								false,
 								StageElementBehaviour::Check,
-								Some(captures[2].parse::<u8>().unwrap())
+								Some(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::JumpLimitParseInt)?)
 							)
 						),
 					),
 					"WormholeIn"=>{
 						force_can_collide=false;
 						force_intersecting=true;
-						assert!(wormhole_in_model_to_id.insert(model_id,captures[2].parse::<u32>().unwrap()).is_none(),"Impossible");
+						let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeInIdParseInt)?;
+						// It is impossible for two different objects to have the same model id
+						assert!(wormhole_in_model_to_id.insert(model_id,wormhole_id).is_none(),"Impossible");
 					},
-					_=>panic!("regex2[1] messed up bad"),
+					_=>unreachable!("regex2[1] messed up bad"),
 				}
 			}else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
 			.captures(other){
 				force_can_collide=false;
 				force_intersecting=true;
+				let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
 				modes_builder.push_mode_update(
-					ModeId::new(captures[2].parse::<u32>().unwrap()),
+					mode_id,
 					ModeUpdate::zone(
 						model_id,
 						//zone:
 						match &captures[1]{
 							"Finish"=>Zone::Finish,
 							"Anticheat"=>Zone::Anticheat,
-							_=>panic!("regex3[1] messed up bad"),
+							_=>unreachable!("regex3[1] messed up bad"),
 						},
 					),
 				);
@@ -243,7 +245,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 	if allow_booster&&velocity!=vec3::ZERO{
 		general.booster=Some(attr::Booster::Velocity(velocity));
 	}
-	match force_can_collide{
+	Ok(match force_can_collide{
 		true=>{
 			match name{
 				"Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)),
@@ -261,7 +263,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 		}else{
 			attr::CollisionAttributes::Decoration
 		},
-	}
+	})
 }
 
 #[derive(Clone,Copy)]
@@ -407,21 +409,25 @@ fn get_content_url(content:&rbx_dom_weak::types::Content)->Option<&str>{
 }
 
 fn get_texture_description<'a>(
-	temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
 	render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
+	recoverable_errors:&mut RecoverableErrors,
+	db:&rbx_reflection::ReflectionDatabase,
 	dom:&'a rbx_dom_weak::WeakDom,
 	object:&rbx_dom_weak::Instance,
 	size:&rbx_dom_weak::types::Vector3,
 )->RobloxPartDescription{
 	//use the biggest one and cut it down later...
 	let mut part_texture_description=RobloxPartDescription::default();
-	temp_objects.clear();
-	recursive_collect_superclass(temp_objects,&dom,object,"Decal");
-	for &mut decal_ref in temp_objects{
-		let Some(decal)=dom.get_by_ref(decal_ref) else{
-			println!("Decal get_by_ref failed");
-			continue;
-		};
+	let decal=&db.classes["Decal"];
+	let decals=object.children().iter().filter_map(|&referent|{
+		let instance=dom.get_by_ref(referent)?;
+		db.classes.get(instance.class.as_str()).is_some_and(|class|
+			db.has_superclass(class,decal)
+		).then_some(instance)
+	});
+	for decal in decals{
+		// decals should always have these properties,
+		// but it is not guaranteed by the rbx_dom_weak data structure.
 		let (
 			Some(rbx_dom_weak::types::Variant::Content(content)),
 			Some(rbx_dom_weak::types::Variant::Enum(normalid)),
@@ -433,16 +439,16 @@ fn get_texture_description<'a>(
 			decal.properties.get(&static_ustr("Color3")),
 			decal.properties.get(&static_ustr("Transparency")),
 		)else{
-			println!("Decal is missing a required property");
+			recoverable_errors.decal_property.push(InstancePath::new(dom,decal));
 			continue;
 		};
-		let texture_id=match content.value(){
-			rbx_dom_weak::types::ContentType::Uri(uri)=>Some(uri.as_str()),
-			_=>None,
-		};
+		let texture_id=get_content_url(content);
 		let render_id=render_config_deferred_loader.acquire_render_config_id(texture_id);
 		let Ok(cube_face)=normalid.to_u32().try_into()else{
-			println!("NormalId is invalid");
+			recoverable_errors.normal_id.push(NormalIdError{
+				path:InstancePath::new(dom,decal),
+				normal_id:normalid.to_u32(),
+			});
 			continue;
 		};
 		let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
@@ -479,6 +485,7 @@ fn get_texture_description<'a>(
 					}
 				)
 			}else{
+				recoverable_errors.texture_property.push(InstancePath::new(dom,decal));
 				(glam::Vec4::ONE,RobloxTextureTransform::identity())
 			}
 		}else{
@@ -532,6 +539,8 @@ pub fn convert<'a>(
 	render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
 	mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>,
 )->PartialMap1<'a>{
+	let mut recoverable_errors=RecoverableErrors::default();
+
 	let mut deferred_models_deferred_attributes=Vec::new();
 	let mut deferred_unions_deferred_attributes=Vec::new();
 	let mut primitive_models_deferred_attributes=Vec::new();
@@ -541,63 +550,82 @@ pub fn convert<'a>(
 	//just going to leave it like this for now instead of reworking the data structures for this whole thing
 	let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
 
-	let mut object_refs=Vec::new();
-	let mut temp_objects=Vec::new();
-	recursive_collect_superclass(&mut object_refs, &dom, dom.root(),"BasePart");
-	for object_ref in object_refs {
-		if let Some(object)=dom.get_by_ref(object_ref){
-			if let (
-					Some(rbx_dom_weak::types::Variant::CFrame(cf)),
-					Some(rbx_dom_weak::types::Variant::Vector3(size)),
-					Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
-					Some(rbx_dom_weak::types::Variant::Float32(transparency)),
-					Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
-					Some(rbx_dom_weak::types::Variant::Bool(can_collide)),
-				) = (
-					object.properties.get(&static_ustr("CFrame")),
-					object.properties.get(&static_ustr("Size")),
-					object.properties.get(&static_ustr("Velocity")),
-					object.properties.get(&static_ustr("Transparency")),
-					object.properties.get(&static_ustr("Color")),
-					object.properties.get(&static_ustr("CanCollide")),
-				)
-			{
-				let model_transform=planar64_affine3_from_roblox(cf,size);
-
+	let db=rbx_reflection_database::get();
+	let basepart=&db.classes["BasePart"];
+	let baseparts=dom.descendants().filter(|&instance|
+		db.classes.get(instance.class.as_str()).is_some_and(|class|
+			db.has_superclass(class,basepart)
+		)
+	);
+	for object in baseparts{
+		let (
+			Some(rbx_dom_weak::types::Variant::CFrame(cf)),
+			Some(rbx_dom_weak::types::Variant::Vector3(size)),
+			Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
+			Some(rbx_dom_weak::types::Variant::Float32(transparency)),
+			Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
+			Some(&rbx_dom_weak::types::Variant::Bool(can_collide)),
+		) = (
+			object.properties.get(&static_ustr("CFrame")),
+			object.properties.get(&static_ustr("Size")),
+			object.properties.get(&static_ustr("Velocity")),
+			object.properties.get(&static_ustr("Transparency")),
+			object.properties.get(&static_ustr("Color")),
+			object.properties.get(&static_ustr("CanCollide")),
+		)else{
+			recoverable_errors.basepart_property.push(InstancePath::new(dom,object));
+			continue;
+		};
+		let model_transform=match planar64_affine3_from_roblox(cf,size){
+			Ok(model_transform)=>{
 				if model_transform.matrix3.det().is_zero(){
-					let mut parent_ref=object.parent();
-					let mut full_path=object.name.clone();
-					while let Some(parent)=dom.get_by_ref(parent_ref){
-						full_path=format!("{}.{}",parent.name,full_path);
-						parent_ref=parent.parent();
-					}
-					println!("Zero determinant CFrame at location {}",full_path);
-					println!("matrix3:{}",model_transform.matrix3);
+					recoverable_errors.basepart_cframe.push(CFrameError{
+						path:InstancePath::new(dom,object),
+						error:CFrameErrorType::ZeroDeterminant,
+					});
 					continue;
 				}
+				model_transform
+			},
+			Err(e)=>{
+				recoverable_errors.basepart_cframe.push(CFrameError{
+					path:InstancePath::new(dom,object),
+					error:CFrameErrorType::Convert(e),
+				});
+				continue;
+			}
+		};
 
-				//TODO: also detect "CylinderMesh" etc here
-				let shape=match object.class.as_str(){
-					"Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&static_ustr("Shape")){
-						Shape::Primitive(shape.to_u32().try_into().expect("Funky roblox PartType"))
-					}else{
-						panic!("Part has no Shape!");
-					},
-					"TrussPart"=>Shape::Primitive(Primitives::Cube),
-					"WedgePart"=>Shape::Primitive(Primitives::Wedge),
-					"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
-					"MeshPart"=>Shape::MeshPart,
-					"UnionOperation"=>Shape::PhysicsData,
-					_=>{
-						println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class);
-						Shape::Primitive(Primitives::Cube)
-					}
+		//TODO: also detect "CylinderMesh" etc here
+		let shape=match object.class.as_str(){
+			"Part"=>{
+				let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&static_ustr("Shape"))else{
+					recoverable_errors.part_property.push(InstancePath::new(dom,object));
+					continue;
 				};
+				let Ok(shape)=shape.to_u32().try_into()else{
+					recoverable_errors.part_shape.push(ShapeError{
+						path:InstancePath::new(dom,object),
+						shape:shape.to_u32(),
+					});
+					continue;
+				};
+				Shape::Primitive(shape)
+			},
+			"TrussPart"=>Shape::Primitive(Primitives::Cube),
+			"WedgePart"=>Shape::Primitive(Primitives::Wedge),
+			"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
+			"MeshPart"=>Shape::MeshPart,
+			"UnionOperation"=>Shape::PhysicsData,
+			_=>{
+				recoverable_errors.unsupported_class.insert(object.class.as_str().to_owned());
+				Shape::Primitive(Primitives::Cube)
+			}
+		};
 
-				let (availability,mesh_id)=match shape{
-					Shape::Primitive(primitive_shape)=>{
-				//TODO: TAB TAB
-				let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
+		let (availability,mesh_id)=match shape{
+			Shape::Primitive(primitive_shape)=>{
+				let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
 				//obscure rust syntax "slice pattern"
 				let RobloxPartDescription([
 					f0,//Cube::Right
@@ -646,66 +674,83 @@ pub fn convert<'a>(
 					mesh_id
 				};
 				(MeshAvailability::Immediate,mesh_id)
-					},
-					Shape::MeshPart=>if let (
-						Some(rbx_dom_weak::types::Variant::Content(mesh_content)),
-						Some(rbx_dom_weak::types::Variant::Content(texture_content)),
-					)=(
-						// mesh must exist
-						object.properties.get(&static_ustr("MeshContent")),
-						// texture is allowed to be none
-						object.properties.get(&static_ustr("TextureContent")),
-					){
-						let mesh_asset_id=get_content_url(mesh_content).unwrap_or_default();
-						let texture_asset_id=get_content_url(texture_content);
-						(
-							MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
-							mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
-						)
-					}else{
-						panic!("Mesh has no Mesh or Texture");
-					},
-					Shape::PhysicsData=>{
-						let mut content="";
-						let mut mesh_data:&[u8]=&[];
-						let mut physics_data:&[u8]=&[];
-						if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&static_ustr("AssetId")){
-							content=asset_id.as_ref();
-						}
-						if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("MeshData")){
-							mesh_data=data.as_ref();
-						}
-						if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("PhysicsData")){
-							physics_data=data.as_ref();
-						}
-						let part_texture_description=get_texture_description(&mut temp_objects,render_config_deferred_loader,dom,object,size);
-						let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
-						let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
-						(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
-					},
+			},
+			Shape::MeshPart=>{
+				let (
+					Some(rbx_dom_weak::types::Variant::Content(mesh_content)),
+					Some(rbx_dom_weak::types::Variant::Content(texture_content)),
+				)=(
+					// mesh must exist
+					object.properties.get(&static_ustr("MeshContent")),
+					// texture is allowed to be none
+					object.properties.get(&static_ustr("TextureContent")),
+				)else{
+					recoverable_errors.meshpart_property.push(InstancePath::new(dom,object));
+					continue;
 				};
-				let model_deferred_attributes=ModelDeferredAttributes{
-					mesh:mesh_id,
-					transform:model_transform,
-					color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency),
-					deferred_attributes:GetAttributesArgs{
-						name:object.name.as_str(),
-						can_collide:*can_collide,
-						velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(),
-					},
+				let mesh_asset_id=match get_content_url(mesh_content){
+					Some(mesh_asset_id)=>mesh_asset_id,
+					None=>{
+						recoverable_errors.meshpart_content.push(InstancePath::new(dom,object));
+						// Return an empty string which will fail to parse as an asset id
+						""
+					}
 				};
-				match availability{
-					MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
-					MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
-						render,
-						model:model_deferred_attributes
-					}),
-					MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
-						render:part_texture_description,
-						model:model_deferred_attributes,
-					}),
+				let texture_asset_id=get_content_url(texture_content);
+				(
+					MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
+					mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
+				)
+			},
+			Shape::PhysicsData=>{
+				let mut content="";
+				let mut mesh_data:&[u8]=&[];
+				let mut physics_data:&[u8]=&[];
+				if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&static_ustr("AssetId")){
+					content=asset_id.as_ref();
 				}
+				if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("MeshData")){
+					mesh_data=data.as_ref();
+				}
+				if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("PhysicsData")){
+					physics_data=data.as_ref();
+				}
+				let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
+				let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
+				let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
+				(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
+			},
+		};
+		let velocity=match vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]){
+			Ok(velocity)=>velocity,
+			Err(e)=>{
+				recoverable_errors.basepart_velocity.push(Planar64ConvertError{
+					path:InstancePath::new(dom,object),
+					error:e,
+				});
+				continue;
 			}
+		};
+		let model_deferred_attributes=ModelDeferredAttributes{
+			mesh:mesh_id,
+			transform:model_transform,
+			color:glam::vec4(color3.r as f32/255f32,color3.g as f32/255f32,color3.b as f32/255f32,1.0-*transparency),
+			deferred_attributes:GetAttributesArgs{
+				name:object.name.as_str(),
+				can_collide,
+				velocity,
+			},
+		};
+		match availability{
+			MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
+			MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
+				render,
+				model:model_deferred_attributes
+			}),
+			MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
+				render:part_texture_description,
+				model:model_deferred_attributes,
+			}),
 		}
 	}
 	PartialMap1{
@@ -713,6 +758,7 @@ pub fn convert<'a>(
 		primitive_models_deferred_attributes,
 		deferred_models_deferred_attributes,
 		deferred_unions_deferred_attributes,
+		recoverable_errors,
 	}
 }
 struct MeshIdWithSize{
@@ -772,6 +818,7 @@ pub struct PartialMap1<'a>{
 	primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>,
 	deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>,
 	deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
+	recoverable_errors:RecoverableErrors,
 }
 impl PartialMap1<'_>{
 	pub fn add_meshpart_meshes_and_calculate_attributes(
@@ -795,6 +842,7 @@ impl PartialMap1<'_>{
 		// I just want to chain iterators together man
 		let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes);
 
+		let mut model_counter=0;
 		let mut mesh_id_from_render_config_id=HashMap::new();
 		let mut union_id_from_render_config_id=HashMap::new();
 		//now that the meshes are loaded, these models can be generated
@@ -847,61 +895,78 @@ impl PartialMap1<'_>{
 			})
 		}))
 		.chain(self.primitive_models_deferred_attributes.into_iter())
-		.enumerate().map(|(model_id,model_deferred_attributes)|{
-			let model_id=model::ModelId::new(model_id as u32);
-			ModelOwnedAttributes{
+		.filter_map(|model_deferred_attributes|{
+			let model_id=model::ModelId::new(model_counter);
+			let attributes=match get_attributes(
+				&model_deferred_attributes.deferred_attributes.name,
+				model_deferred_attributes.deferred_attributes.can_collide,
+				model_deferred_attributes.deferred_attributes.velocity,
+				model_id,
+				&mut modes_builder,
+				&mut wormhole_in_model_to_id,
+				&mut wormhole_id_to_out_model,
+			){
+				Ok(attributes)=>attributes,
+				Err(e)=>{
+					match e{
+						GetAttributesError::ModeIdParseInt(e)=>self.recoverable_errors.mode_id_parse_int.push(e),
+						GetAttributesError::DuplicateMode(mode_id)=>{self.recoverable_errors.duplicate_mode.insert(mode_id);},
+						GetAttributesError::StageIdParseInt(e)=>self.recoverable_errors.stage_id_parse_int.push(e),
+						GetAttributesError::DuplicateStage(duplicate_stage)=>{self.recoverable_errors.duplicate_stage.insert(duplicate_stage);},
+						GetAttributesError::WormholeOutIdParseInt(e)=>self.recoverable_errors.wormhole_out_id_parse_int.push(e),
+						GetAttributesError::DuplicateWormholeOut(wormhole_id)=>{self.recoverable_errors.duplicate_wormhole_out.insert(wormhole_id);},
+						GetAttributesError::WormholeInIdParseInt(e)=>self.recoverable_errors.wormhole_in_id_parse_int.push(e),
+						GetAttributesError::JumpLimitParseInt(e)=>self.recoverable_errors.jump_limit_parse_int.push(e),
+					}
+					return None;
+				}
+			};
+			model_counter+=1;
+			Some(ModelOwnedAttributes{
 				mesh:model_deferred_attributes.mesh,
-				attributes:get_attributes(
-					&model_deferred_attributes.deferred_attributes.name,
-					model_deferred_attributes.deferred_attributes.can_collide,
-					model_deferred_attributes.deferred_attributes.velocity,
-					model_id,
-					&mut modes_builder,
-					&mut wormhole_in_model_to_id,
-					&mut wormhole_id_to_out_model,
-				),
+				attributes,
 				color:model_deferred_attributes.color,
 				transform:model_deferred_attributes.transform,
-			}
+			})
 		}).collect();
 		let models=models_owned_attributes.into_iter().enumerate().map(|(model_id,mut model_owned_attributes)|{
-		//TODO: TAB
-		let model_id=model::ModelId::new(model_id as u32);
-		//update attributes with wormhole id
-		//TODO: errors/prints
-		if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){
-			if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){
-				match &mut model_owned_attributes.attributes{
-					attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general})
-					|attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general})
-					=>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}),
-					attr::CollisionAttributes::Decoration=>println!("Not a wormhole"),
+			let model_id=model::ModelId::new(model_id as u32);
+			//update attributes with wormhole id
+			//TODO: errors/prints
+			if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){
+				if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){
+					match &mut model_owned_attributes.attributes{
+						attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general})
+						|attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general})
+						=>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}),
+						attr::CollisionAttributes::Decoration=>println!("Not a wormhole"),
+					}
 				}
 			}
-		}
 
-		//index the attributes
-		let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){
-			attributes_id
-		}else{
-			let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32);
-			attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id);
-			unique_attributes.push(model_owned_attributes.attributes);
-			attributes_id
-		};
-		model::Model{
-			mesh:model_owned_attributes.mesh,
-			transform:model_owned_attributes.transform,
-			color:model_owned_attributes.color,
-			attributes:attributes_id,
+			//index the attributes
+			let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){
+				attributes_id
+			}else{
+				let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32);
+				attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id);
+				unique_attributes.push(model_owned_attributes.attributes);
+				attributes_id
+			};
+			model::Model{
+				mesh:model_owned_attributes.mesh,
+				transform:model_owned_attributes.transform,
+				color:model_owned_attributes.color,
+				attributes:attributes_id,
+			}
+		}).collect();
+		PartialMap2{
+			meshes:self.primitive_meshes,
+			models,
+			modes:modes_builder.build_normalized(),
+			attributes:unique_attributes,
+			recoverable_errors:self.recoverable_errors,
 		}
-	}).collect();
-	PartialMap2{
-		meshes:self.primitive_meshes,
-		models,
-		modes:modes_builder.build_normalized(),
-		attributes:unique_attributes,
-	}
 	}
 }
 
@@ -910,12 +975,13 @@ pub struct PartialMap2{
 	models:Vec<model::Model>,
 	modes:NormalizedModes,
 	attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
+	recoverable_errors:RecoverableErrors,
 }
 impl PartialMap2{
 	pub fn add_render_configs_and_textures(
 		self,
 		render_configs:RenderConfigs,
-	)->map::CompleteMap{
+	)->(map::CompleteMap,RecoverableErrors){
 		let (textures,render_configs)=render_configs.consume();
 		let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>)
 		=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
@@ -934,14 +1000,17 @@ impl PartialMap2{
 			);
 			render_config
 		}).collect();
-		map::CompleteMap{
-			modes:self.modes,
-			attributes:self.attributes,
-			meshes:self.meshes,
-			models:self.models,
-			//the roblox legacy texture thing always works
-			textures,
-			render_configs,
-		}
+		(
+			map::CompleteMap{
+				modes:self.modes,
+				attributes:self.attributes,
+				meshes:self.meshes,
+				models:self.models,
+				//the roblox legacy texture thing always works
+				textures,
+				render_configs,
+			},
+			self.recoverable_errors,
+		)
 	}
 }