diff --git a/lib/common/src/gameplay_modes.rs b/lib/common/src/gameplay_modes.rs
index d0d204bde..b9bdb24c5 100644
--- a/lib/common/src/gameplay_modes.rs
+++ b/lib/common/src/gameplay_modes.rs
@@ -128,18 +128,6 @@ impl Stage{
 		self.unordered_checkpoints.contains(&model_id)
 	}
 }
-#[derive(Default)]
-pub struct StageUpdate{
-	//other behaviour models of this stage can have
-	ordered_checkpoints:HashMap<CheckpointId,ModelId>,
-	unordered_checkpoints:HashSet<ModelId>,
-}
-impl Updatable<StageUpdate> for Stage{
-	fn update(&mut self,update:StageUpdate){
-		self.ordered_checkpoints.extend(update.ordered_checkpoints);
-		self.unordered_checkpoints.extend(update.unordered_checkpoints);
-	}
-}
 
 #[derive(Clone,Copy,Hash,Eq,PartialEq)]
 pub enum Zone{
@@ -256,47 +244,6 @@ impl Mode{
 		}
 	}
 }
-//this would be nice as a macro
-#[derive(Default)]
-pub struct ModeUpdate{
-	zones:HashMap<ModelId,Zone>,
-	stages:HashMap<StageId,StageUpdate>,
-	//mutually exlusive stage element behaviour
-	elements:HashMap<ModelId,StageElement>,
-}
-impl Updatable<ModeUpdate> for Mode{
-	fn update(&mut self,update:ModeUpdate){
-		self.zones.extend(update.zones);
-		for (stage,stage_update) in update.stages{
-			if let Some(stage)=self.stages.get_mut(stage.0 as usize){
-				stage.update(stage_update);
-			}
-		}
-		self.elements.extend(update.elements);
-	}
-}
-impl ModeUpdate{
-	pub fn zone(model_id:ModelId,zone:Zone)->Self{
-		let mut mu=Self::default();
-		mu.zones.insert(model_id,zone);
-		mu
-	}
-	pub fn stage(stage_id:StageId,stage_update:StageUpdate)->Self{
-		let mut mu=Self::default();
-		mu.stages.insert(stage_id,stage_update);
-		mu
-	}
-	pub fn element(model_id:ModelId,element:StageElement)->Self{
-		let mut mu=Self::default();
-		mu.elements.insert(model_id,element);
-		mu
-	}
-	pub fn map_stage_element_ids<F:Fn(StageId)->StageId>(&mut self,f:F){
-		for (_,stage_element) in self.elements.iter_mut(){
-			stage_element.stage_id=f(stage_element.stage_id);
-		}
-	}
-}
 
 #[derive(Default,Clone)]
 pub struct Modes{
@@ -318,15 +265,165 @@ impl Modes{
 		self.modes.get(mode.0 as usize)
 	}
 }
+
+
+#[derive(Default)]
+pub struct StageUpdate{
+	//other behaviour models of this stage can have
+	ordered_checkpoints:HashMap<CheckpointId,ModelId>,
+	unordered_checkpoints:HashSet<ModelId>,
+}
+impl Updatable<StageUpdate> for Stage{
+	fn update(&mut self,update:StageUpdate){
+		self.ordered_checkpoints.extend(update.ordered_checkpoints);
+		self.unordered_checkpoints.extend(update.unordered_checkpoints);
+	}
+}
+
+//this would be nice as a macro
+#[derive(Default)]
+pub struct ModeUpdate{
+	zones:Option<(ModelId,Zone)>,
+	stages:Option<(StageId,StageUpdate)>,
+	//mutually exlusive stage element behaviour
+	elements:Option<(ModelId,StageElement)>,
+}
+impl Updatable<ModeUpdate> for Mode{
+	fn update(&mut self,update:ModeUpdate){
+		self.zones.extend(update.zones);
+		if let Some((StageId(stage_id),stage_update))=update.stages{
+			if let Some(stage)=self.stages.get_mut(stage_id as usize){
+				stage.update(stage_update);
+			}
+		}
+		self.elements.extend(update.elements);
+	}
+}
+impl ModeUpdate{
+	pub fn zone(model_id:ModelId,zone:Zone)->Self{
+		Self{
+			zones:Some((model_id,zone)),
+			stages:None,
+			elements:None,
+		}
+	}
+	pub fn stage(stage_id:StageId,stage_update:StageUpdate)->Self{
+		Self{
+			zones:None,
+			stages:Some((stage_id,stage_update)),
+			elements:None,
+		}
+	}
+	pub fn element(model_id:ModelId,element:StageElement)->Self{
+		Self{
+			zones:None,
+			stages:None,
+			elements:Some((model_id,element)),
+		}
+	}
+	pub fn map_stage_element_ids<F:Fn(StageId)->StageId>(&mut self,f:F){
+		for (_,stage_element) in self.elements.iter_mut(){
+			stage_element.stage_id=f(stage_element.stage_id);
+		}
+	}
+}
 pub struct ModesUpdate{
 	modes:HashMap<ModeId,ModeUpdate>,
 }
 impl Updatable<ModesUpdate> for Modes{
 	fn update(&mut self,update:ModesUpdate){
-		for (mode,mode_update) in update.modes{
-			if let Some(mode)=self.modes.get_mut(mode.0 as usize){
+		for (ModeId(mode_id),mode_update) in update.modes{
+			if let Some(mode)=self.modes.get_mut(mode_id as usize){
 				mode.update(mode_update);
 			}
 		}
 	}
 }
+
+struct ModeBuilder{
+	mode:Mode,
+	final_stage_id_from_builder_stage_id:HashMap<StageId,StageId>,
+}
+#[derive(Default)]
+pub struct ModesBuilder{
+	modes:HashMap<ModeId,Mode>,
+	stages:HashMap<ModeId,HashMap<StageId,Stage>>,
+	mode_updates:Vec<(ModeId,ModeUpdate)>,
+	stage_updates:Vec<(ModeId,StageId,StageUpdate)>,
+}
+impl ModesBuilder{
+	pub fn build(mut self)->Modes{
+		//collect modes and stages into contiguous arrays
+		let mut unique_modes:Vec<(ModeId,Mode)>
+		=self.modes.into_iter().collect();
+		unique_modes.sort_by_key(|&(mode_id,_)|mode_id);
+		let (mut modes,final_mode_id_from_builder_mode_id):(Vec<ModeBuilder>,HashMap<ModeId,ModeId>)
+		=unique_modes.into_iter().enumerate()
+		.map(|(final_mode_id,(builder_mode_id,mut mode))|{
+			(
+				ModeBuilder{
+					final_stage_id_from_builder_stage_id:self.stages.remove(&builder_mode_id).map_or_else(||HashMap::new(),|stages|{
+						let mut unique_stages:Vec<(StageId,Stage)>
+						=stages.into_iter().collect();
+						unique_stages.sort_by(|a,b|a.0.cmp(&b.0));
+						unique_stages.into_iter().enumerate()
+						.map(|(final_stage_id,(builder_stage_id,stage))|{
+							mode.push_stage(stage);
+							(builder_stage_id,StageId::new(final_stage_id as u32))
+						}).collect()
+					}),
+					mode,
+				},
+				(
+					builder_mode_id,
+					ModeId::new(final_mode_id as u32)
+				)
+			)
+		}).unzip();
+		//TODO: failure messages or errors or something
+		//push stage updates
+		for (builder_mode_id,builder_stage_id,stage_update) in self.stage_updates{
+			if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){
+				if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
+					if let Some(&final_stage_id)=mode.final_stage_id_from_builder_stage_id.get(&builder_stage_id){
+						if let Some(stage)=mode.mode.get_stage_mut(final_stage_id){
+							stage.update(stage_update);
+						}
+					}
+				}
+			}
+		}
+		//push mode updates
+		for (builder_mode_id,mut mode_update) in self.mode_updates{
+			if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){
+				if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
+					//map stage id on stage elements
+					mode_update.map_stage_element_ids(|stage_id|
+						//walk down one stage id at a time until a stage is found
+						//TODO use better logic like BTreeMap::upper_bound instead of walking
+						// final_stage_id_from_builder_stage_id.upper_bound(Bound::Included(&stage_id))
+						// .value().copied().unwrap_or(StageId::FIRST)
+						(0..=stage_id.get()).rev().find_map(|builder_stage_id|
+							//map the stage element to that stage
+							mode.final_stage_id_from_builder_stage_id.get(&StageId::new(builder_stage_id)).copied()
+						).unwrap_or(StageId::FIRST)
+					);
+					mode.mode.update(mode_update);
+				}
+			}
+		}
+		Modes::new(modes.into_iter().map(|mode_builder|mode_builder.mode).collect())
+	}
+	pub fn insert_mode(&mut self,mode_id:ModeId,mode:Mode){
+		assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode");
+	}
+	pub fn insert_stage(&mut self,mode_id:ModeId,stage_id:StageId,stage:Stage){
+		assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage");
+	}
+	pub fn push_mode_update(&mut self,mode_id:ModeId,mode_update:ModeUpdate){
+		self.mode_updates.push((mode_id,mode_update));
+	}
+	// fn push_stage_update(&mut self,mode_id:ModeId,stage_id:StageId,stage_update:StageUpdate){
+	// 	self.stage_updates.push((mode_id,stage_id,stage_update));
+	// }
+}
diff --git a/lib/rbx_loader/src/rbx.rs b/lib/rbx_loader/src/rbx.rs
index 8162662aa..208394d0e 100644
--- a/lib/rbx_loader/src/rbx.rs
+++ b/lib/rbx_loader/src/rbx.rs
@@ -4,12 +4,11 @@ use crate::primitives;
 use strafesnet_common::aabb::Aabb;
 use strafesnet_common::map;
 use strafesnet_common::model;
-use strafesnet_common::gameplay_modes;
+use strafesnet_common::gameplay_modes::{Mode,ModeId,ModeUpdate,Modes,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::model::RenderConfigId;
-use strafesnet_common::updatable::Updatable;
 use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
 use strafesnet_deferred_loader::mesh::Meshes;
 use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
@@ -52,93 +51,7 @@ fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_we
 		vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z]).unwrap()
 	)
 }
-struct ModeBuilder{
-	mode:gameplay_modes::Mode,
-	final_stage_id_from_builder_stage_id:HashMap<gameplay_modes::StageId,gameplay_modes::StageId>,
-}
-#[derive(Default)]
-struct ModesBuilder{
-	modes:HashMap<gameplay_modes::ModeId,gameplay_modes::Mode>,
-	stages:HashMap<gameplay_modes::ModeId,HashMap<gameplay_modes::StageId,gameplay_modes::Stage>>,
-	mode_updates:Vec<(gameplay_modes::ModeId,gameplay_modes::ModeUpdate)>,
-	stage_updates:Vec<(gameplay_modes::ModeId,gameplay_modes::StageId,gameplay_modes::StageUpdate)>,
-}
-impl ModesBuilder{
-	fn build(mut self)->gameplay_modes::Modes{
-		//collect modes and stages into contiguous arrays
-		let mut unique_modes:Vec<(gameplay_modes::ModeId,gameplay_modes::Mode)>
-		=self.modes.into_iter().collect();
-		unique_modes.sort_by_key(|&(mode_id,_)|mode_id);
-		let (mut modes,final_mode_id_from_builder_mode_id):(Vec<ModeBuilder>,HashMap<gameplay_modes::ModeId,gameplay_modes::ModeId>)
-		=unique_modes.into_iter().enumerate()
-		.map(|(final_mode_id,(builder_mode_id,mut mode))|{
-			(
-				ModeBuilder{
-					final_stage_id_from_builder_stage_id:self.stages.remove(&builder_mode_id).map_or_else(||HashMap::new(),|stages|{
-						let mut unique_stages:Vec<(gameplay_modes::StageId,gameplay_modes::Stage)>
-						=stages.into_iter().collect();
-						unique_stages.sort_by(|a,b|a.0.cmp(&b.0));
-						unique_stages.into_iter().enumerate()
-						.map(|(final_stage_id,(builder_stage_id,stage))|{
-							mode.push_stage(stage);
-							(builder_stage_id,gameplay_modes::StageId::new(final_stage_id as u32))
-						}).collect()
-					}),
-					mode,
-				},
-				(
-					builder_mode_id,
-					gameplay_modes::ModeId::new(final_mode_id as u32)
-				)
-			)
-		}).unzip();
-		//TODO: failure messages or errors or something
-		//push stage updates
-		for (builder_mode_id,builder_stage_id,stage_update) in self.stage_updates{
-			if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){
-				if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
-					if let Some(&final_stage_id)=mode.final_stage_id_from_builder_stage_id.get(&builder_stage_id){
-						if let Some(stage)=mode.mode.get_stage_mut(final_stage_id){
-							stage.update(stage_update);
-						}
-					}
-				}
-			}
-		}
-		//push mode updates
-		for (builder_mode_id,mut mode_update) in self.mode_updates{
-			if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){
-				if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){
-					//map stage id on stage elements
-					mode_update.map_stage_element_ids(|stage_id|
-						//walk down one stage id at a time until a stage is found
-						//TODO use better logic like BTreeMap::upper_bound instead of walking
-						// final_stage_id_from_builder_stage_id.upper_bound(Bound::Included(&stage_id))
-						// .value().copied().unwrap_or(gameplay_modes::StageId::FIRST)
-						(0..=stage_id.get()).rev().find_map(|builder_stage_id|
-							//map the stage element to that stage
-							mode.final_stage_id_from_builder_stage_id.get(&gameplay_modes::StageId::new(builder_stage_id)).copied()
-						).unwrap_or(gameplay_modes::StageId::FIRST)
-					);
-					mode.mode.update(mode_update);
-				}
-			}
-		}
-		gameplay_modes::Modes::new(modes.into_iter().map(|mode_builder|mode_builder.mode).collect())
-	}
-	fn insert_mode(&mut self,mode_id:gameplay_modes::ModeId,mode:gameplay_modes::Mode){
-		assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode");
-	}
-	fn insert_stage(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage:gameplay_modes::Stage){
-		assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage");
-	}
-	fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){
-		self.mode_updates.push((mode_id,mode_update));
-	}
-	// fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){
-	// 	self.stage_updates.push((mode_id,stage_id,stage_update));
-	// }
-}
+
 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{
 	let mut general=attr::GeneralAttributes::default();
 	let mut intersecting=attr::IntersectingAttributes::default();
@@ -167,8 +80,8 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 			force_can_collide=false;
 			force_intersecting=true;
 			modes_builder.insert_mode(
-				gameplay_modes::ModeId::MAIN,
-				gameplay_modes::Mode::empty(
+				ModeId::MAIN,
+				Mode::empty(
 					gameplay_style::StyleModifiers::roblox_bhop(),
 					model_id
 				)
@@ -178,10 +91,10 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 			force_can_collide=false;
 			force_intersecting=true;
 			modes_builder.push_mode_update(
-				gameplay_modes::ModeId::MAIN,
-				gameplay_modes::ModeUpdate::zone(
+				ModeId::MAIN,
+				ModeUpdate::zone(
 					model_id,
-					gameplay_modes::Zone::Finish,
+					Zone::Finish,
 				),
 			);
 		},
@@ -189,19 +102,19 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 			force_can_collide=false;
 			force_intersecting=true;
 			modes_builder.push_mode_update(
-				gameplay_modes::ModeId::MAIN,
-				gameplay_modes::ModeUpdate::zone(
+				ModeId::MAIN,
+				ModeUpdate::zone(
 					model_id,
-					gameplay_modes::Zone::Anticheat,
+					Zone::Anticheat,
 				),
 			);
 		},
 		"Platform"=>{
 			modes_builder.push_mode_update(
-				gameplay_modes::ModeId::MAIN,
-				gameplay_modes::ModeUpdate::element(
+				ModeId::MAIN,
+				ModeUpdate::element(
 					model_id,
-					gameplay_modes::StageElement::new(gameplay_modes::StageId::FIRST,false,gameplay_modes::StageElementBehaviour::Platform,None),//roblox does not know which stage the platform belongs to
+					StageElement::new(StageId::FIRST,false,StageElementBehaviour::Platform,None),//roblox does not know which stage the platform belongs to
 				),
 			);
 		},
@@ -213,8 +126,8 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 						force_can_collide=false;
 						force_intersecting=true;
 						modes_builder.insert_mode(
-							gameplay_modes::ModeId::new(captures[2].parse::<u32>().unwrap()),
-							gameplay_modes::Mode::empty(
+							ModeId::new(captures[2].parse::<u32>().unwrap()),
+							Mode::empty(
 								gameplay_style::StyleModifiers::roblox_bhop(),
 								model_id
 							)
@@ -231,8 +144,8 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 			}else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
 			.captures(other){
 				force_intersecting=true;
-				let stage_id=gameplay_modes::StageId::new(captures[3].parse::<u32>().unwrap());
-				let stage_element=gameplay_modes::StageElement::new(
+				let stage_id=StageId::new(captures[3].parse::<u32>().unwrap());
+				let stage_element=StageElement::new(
 					//stage_id:
 					stage_id,
 					//force:
@@ -244,26 +157,26 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 					match &captures[2]{
 						"Spawn"=>{
 							modes_builder.insert_stage(
-								gameplay_modes::ModeId::MAIN,
+								ModeId::MAIN,
 								stage_id,
-								gameplay_modes::Stage::empty(model_id),
+								Stage::empty(model_id),
 							);
 							//TODO: let denormalize handle this
-							gameplay_modes::StageElementBehaviour::SpawnAt
+							StageElementBehaviour::SpawnAt
 						},
-						"SpawnAt"=>gameplay_modes::StageElementBehaviour::SpawnAt,
+						"SpawnAt"=>StageElementBehaviour::SpawnAt,
 						//cancollide false so you don't hit the side
 						//NOT a decoration
-						"Trigger"=>{force_can_collide=false;gameplay_modes::StageElementBehaviour::Trigger},
-						"Teleport"=>{force_can_collide=false;gameplay_modes::StageElementBehaviour::Teleport},
-						"Platform"=>gameplay_modes::StageElementBehaviour::Platform,
+						"Trigger"=>{force_can_collide=false;StageElementBehaviour::Trigger},
+						"Teleport"=>{force_can_collide=false;StageElementBehaviour::Teleport},
+						"Platform"=>StageElementBehaviour::Platform,
 						_=>panic!("regex1[2] messed up bad"),
 					},
 					None
 				);
 				modes_builder.push_mode_update(
-					gameplay_modes::ModeId::MAIN,
-					gameplay_modes::ModeUpdate::element(
+					ModeId::MAIN,
+					ModeUpdate::element(
 						model_id,
 						stage_element,
 					),
@@ -272,14 +185,14 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 			.captures(other){
 				match &captures[1]{
 					"Jump"=>modes_builder.push_mode_update(
-						gameplay_modes::ModeId::MAIN,
-						gameplay_modes::ModeUpdate::element(
+						ModeId::MAIN,
+						ModeUpdate::element(
 							model_id,
 							//jump_limit:
-							gameplay_modes::StageElement::new(
-								gameplay_modes::StageId::FIRST,
+							StageElement::new(
+								StageId::FIRST,
 								false,
-								gameplay_modes::StageElementBehaviour::Check,
+								StageElementBehaviour::Check,
 								Some(captures[2].parse::<u8>().unwrap())
 							)
 						),
@@ -296,13 +209,13 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 				force_can_collide=false;
 				force_intersecting=true;
 				modes_builder.push_mode_update(
-					gameplay_modes::ModeId::new(captures[2].parse::<u32>().unwrap()),
-					gameplay_modes::ModeUpdate::zone(
+					ModeId::new(captures[2].parse::<u32>().unwrap()),
+					ModeUpdate::zone(
 						model_id,
 						//zone:
 						match &captures[1]{
-							"Finish"=>gameplay_modes::Zone::Finish,
-							"Anticheat"=>gameplay_modes::Zone::Anticheat,
+							"Finish"=>Zone::Finish,
+							"Anticheat"=>Zone::Anticheat,
 							_=>panic!("regex3[1] messed up bad"),
 						},
 					),
@@ -312,9 +225,9 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
 			// .captures(other){
 			// 	match &captures[1]{
 			// 		"OrderedCheckpoint"=>modes_builder.push_stage_update(
-			// 			gameplay_modes::ModeId::MAIN,
-			// 			gameplay_modes::StageId::new(0),
-			// 			gameplay_modes::StageUpdate::ordered_checkpoint(captures[2].parse::<u32>().unwrap()),
+			// 			ModeId::MAIN,
+			// 			StageId::new(0),
+			// 			StageUpdate::ordered_checkpoint(captures[2].parse::<u32>().unwrap()),
 			// 		),
 			// 		_=>panic!("regex3[1] messed up bad"),
 			// 	}
@@ -1018,7 +931,7 @@ impl PartialMap1<'_>{
 pub struct PartialMap2{
 	meshes:Vec<model::Mesh>,
 	models:Vec<model::Model>,
-	modes:gameplay_modes::Modes,
+	modes:Modes,
 	attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
 }
 impl PartialMap2{