diff --git a/lib/common/.gitignore b/lib/common/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/lib/common/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/lib/common/Cargo.lock b/lib/common/Cargo.lock
new file mode 100644
index 0000000..4e65dd8
--- /dev/null
+++ b/lib/common/Cargo.lock
@@ -0,0 +1,121 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "bnum"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50202def95bf36cb7d1d7a7962cea1c36a3f8ad42425e5d2b71d7acb8041b5b8"
+
+[[package]]
+name = "fixed_wide"
+version = "0.1.1"
+source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
+checksum = "d9c2cf115b3785ede870fada07e8b1aeba3378345b4ca86fe3c772ecabc05c0f"
+dependencies = [
+ "arrayvec",
+ "bnum",
+ "paste",
+ "ratio_ops",
+]
+
+[[package]]
+name = "glam"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28091a37a5d09b555cb6628fd954da299b536433834f5b8e59eba78e0cbbf8a"
+
+[[package]]
+name = "id"
+version = "0.1.0"
+source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
+checksum = "2337e7a6c273082b672e377e159d7a168fb51438461b7c4033c79a515dd7a25a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "linear_ops"
+version = "0.1.0"
+source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
+checksum = "b2e6977ac24f47086d8a7a2d4ae1c720e86dfdc8407cf5e34c18bfa01053c456"
+dependencies = [
+ "fixed_wide",
+ "paste",
+ "ratio_ops",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ratio_ops"
+version = "0.1.0"
+source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
+checksum = "01239195d6afe0509e7e3511b716c0540251dfe7ece0a9a5a27116afb766c42c"
+
+[[package]]
+name = "strafesnet_common"
+version = "0.5.2"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "fixed_wide",
+ "glam",
+ "id",
+ "linear_ops",
+ "ratio_ops",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
diff --git a/lib/common/Cargo.toml b/lib/common/Cargo.toml
new file mode 100644
index 0000000..3729001
--- /dev/null
+++ b/lib/common/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "strafesnet_common"
+version = "0.5.2"
+edition = "2021"
+repository = "https://git.itzana.me/StrafesNET/common"
+license = "MIT OR Apache-2.0"
+description = "Common types and helpers for Strafe Client associated projects."
+authors = ["Rhys Lloyd <krakow20@gmail.com>"]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+arrayvec = "0.7.4"
+bitflags = "2.6.0"
+fixed_wide = { version = "0.1.1", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
+linear_ops = { version = "0.1.0", registry = "strafesnet", features = ["deferred-division","named-fields"] }
+ratio_ops = { version = "0.1.0", registry = "strafesnet" }
+glam = "0.29.0"
+id = { version = "0.1.0", registry = "strafesnet" }
diff --git a/lib/common/LICENSE-APACHE b/lib/common/LICENSE-APACHE
new file mode 100644
index 0000000..a7e77cb
--- /dev/null
+++ b/lib/common/LICENSE-APACHE
@@ -0,0 +1,176 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/lib/common/LICENSE-MIT b/lib/common/LICENSE-MIT
new file mode 100644
index 0000000..468cd79
--- /dev/null
+++ b/lib/common/LICENSE-MIT
@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/lib/common/README.md b/lib/common/README.md
new file mode 100644
index 0000000..6823d6c
--- /dev/null
+++ b/lib/common/README.md
@@ -0,0 +1,19 @@
+StrafesNET Common Library
+=========================
+
+## Common types used in the StrafesNET ecosystem
+
+#### License
+
+<sup>
+Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
+2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
+</sup>
+
+<br>
+
+<sub>
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
+be dual licensed as above, without any additional terms or conditions.
+</sub>
\ No newline at end of file
diff --git a/lib/common/src/aabb.rs b/lib/common/src/aabb.rs
new file mode 100644
index 0000000..5e244d2
--- /dev/null
+++ b/lib/common/src/aabb.rs
@@ -0,0 +1,56 @@
+use crate::integer::{vec3,Planar64Vec3};
+
+#[derive(Clone)]
+pub struct Aabb{
+	min:Planar64Vec3,
+	max:Planar64Vec3,
+}
+
+impl Default for Aabb{
+	fn default()->Self{
+		Self{min:vec3::MAX,max:vec3::MIN}
+	}
+}
+
+impl Aabb{
+	pub const fn new(min:Planar64Vec3,max:Planar64Vec3)->Self{
+		Self{min,max}
+	}
+	pub const fn max(&self)->Planar64Vec3{
+		self.max
+	}
+	pub const fn min(&self)->Planar64Vec3{
+		self.min
+	}
+	pub fn grow(&mut self,point:Planar64Vec3){
+		self.min=self.min.min(point);
+		self.max=self.max.max(point);
+	}
+	pub fn join(&mut self,aabb:&Aabb){
+		self.min=self.min.min(aabb.min);
+		self.max=self.max.max(aabb.max);
+	}
+	pub fn inflate(&mut self,hs:Planar64Vec3){
+		self.min-=hs;
+		self.max+=hs;
+	}
+	pub fn intersects(&self,aabb:&Aabb)->bool{
+		let bvec=self.min.lt(aabb.max)&aabb.min.lt(self.max);
+		bvec.all()
+	}
+	pub fn size(&self)->Planar64Vec3{
+		self.max-self.min
+	}
+	pub fn center(&self)->Planar64Vec3{
+		self.min+(self.max-self.min)>>1
+	}
+	//probably use floats for area & volume because we don't care about precision
+	// pub fn area_weight(&self)->f32{
+	// 	let d=self.max-self.min;
+	// 	d.x*d.y+d.y*d.z+d.z*d.x
+	// }
+	// pub fn volume(&self)->f32{
+	// 	let d=self.max-self.min;
+	// 	d.x*d.y*d.z
+	// }
+}
diff --git a/lib/common/src/bvh.rs b/lib/common/src/bvh.rs
new file mode 100644
index 0000000..66d0b2e
--- /dev/null
+++ b/lib/common/src/bvh.rs
@@ -0,0 +1,194 @@
+use crate::aabb::Aabb;
+
+//da algaritum
+//lista boxens
+//sort by {minx,maxx,miny,maxy,minz,maxz} (6 lists)
+//find the sets that minimizes the sum of surface areas
+//splitting is done when the minimum split sum of surface areas is larger than the node's own surface area
+
+//start with bisection into octrees because a bad bvh is still 1000x better than no bvh
+//sort the centerpoints on each axis (3 lists)
+//bv is put into octant based on whether it is upper or lower in each list
+
+pub enum RecursiveContent<R,T>{
+	Branch(Vec<R>),
+	Leaf(T),
+}
+impl<R,T> Default for RecursiveContent<R,T>{
+	fn default()->Self{
+		Self::Branch(Vec::new())
+	}
+}
+pub struct BvhNode<T>{
+	content:RecursiveContent<BvhNode<T>,T>,
+	aabb:Aabb,
+}
+impl<T> Default for BvhNode<T>{
+	fn default()->Self{
+		Self{
+			content:Default::default(),
+			aabb:Aabb::default(),
+		}
+	}
+}
+pub struct BvhWeightNode<W,T>{
+	content:RecursiveContent<BvhWeightNode<W,T>,T>,
+	weight:W,
+	aabb:Aabb,
+}
+
+impl<T> BvhNode<T>{
+	pub fn the_tester<F:FnMut(&T)>(&self,aabb:&Aabb,f:&mut F){
+		match &self.content{
+			RecursiveContent::Leaf(model)=>f(model),
+			RecursiveContent::Branch(children)=>for child in children{
+				//this test could be moved outside the match statement
+				//but that would test the root node aabb
+				//you're probably not going to spend a lot of time outside the map,
+				//so the test is extra work for nothing
+				if aabb.intersects(&child.aabb){
+					child.the_tester(aabb,f);
+				}
+			},
+		}
+	}
+	pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
+		match self.content{
+			RecursiveContent::Leaf(model)=>f(model),
+			RecursiveContent::Branch(children)=>for child in children{
+				child.into_visitor(f)
+			},
+		}
+	}
+	pub fn weigh_contents<W:Copy+std::iter::Sum<W>,F:Fn(&T)->W>(self,f:&F)->BvhWeightNode<W,T>{
+		match self.content{
+			RecursiveContent::Leaf(model)=>BvhWeightNode{
+				weight:f(&model),
+				content:RecursiveContent::Leaf(model),
+				aabb:self.aabb,
+			},
+			RecursiveContent::Branch(children)=>{
+				let branch:Vec<BvhWeightNode<W,T>>=children.into_iter().map(|child|
+					child.weigh_contents(f)
+				).collect();
+				BvhWeightNode{
+					weight:branch.iter().map(|node|node.weight).sum(),
+					content:RecursiveContent::Branch(branch),
+					aabb:self.aabb,
+				}
+			},
+		}
+	}
+}
+
+impl <W,T> BvhWeightNode<W,T>{
+	pub const fn weight(&self)->&W{
+		&self.weight
+	}
+	pub const fn aabb(&self)->&Aabb{
+		&self.aabb
+	}
+	pub fn into_content(self)->RecursiveContent<BvhWeightNode<W,T>,T>{
+		self.content
+	}
+	pub fn into_visitor<F:FnMut(T)>(self,f:&mut F){
+		match self.content{
+			RecursiveContent::Leaf(model)=>f(model),
+			RecursiveContent::Branch(children)=>for child in children{
+				child.into_visitor(f)
+			},
+		}
+	}
+}
+
+pub fn generate_bvh<T>(boxen:Vec<(T,Aabb)>)->BvhNode<T>{
+	generate_bvh_node(boxen,false)
+}
+
+fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
+	let n=boxen.len();
+	if force||n<20{
+		let mut aabb=Aabb::default();
+		let nodes=boxen.into_iter().map(|b|{
+			aabb.join(&b.1);
+			BvhNode{
+				content:RecursiveContent::Leaf(b.0),
+				aabb:b.1,
+			}
+		}).collect();
+		BvhNode{
+			content:RecursiveContent::Branch(nodes),
+			aabb,
+		}
+	}else{
+		let mut sort_x=Vec::with_capacity(n);
+		let mut sort_y=Vec::with_capacity(n);
+		let mut sort_z=Vec::with_capacity(n);
+		for (i,(_,aabb)) in boxen.iter().enumerate(){
+			let center=aabb.center();
+			sort_x.push((i,center.x));
+			sort_y.push((i,center.y));
+			sort_z.push((i,center.z));
+		}
+		sort_x.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
+		sort_y.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
+		sort_z.sort_by(|tup0,tup1|tup0.1.cmp(&tup1.1));
+		let h=n/2;
+		let median_x=sort_x[h].1;
+		let median_y=sort_y[h].1;
+		let median_z=sort_z[h].1;
+		//locate a run of values equal to the median
+		//partition point gives the first index for which the predicate evaluates to false
+		let first_index_eq_median_x=sort_x.partition_point(|&(_,x)|x<median_x);
+		let first_index_eq_median_y=sort_y.partition_point(|&(_,y)|y<median_y);
+		let first_index_eq_median_z=sort_z.partition_point(|&(_,z)|z<median_z);
+		let first_index_gt_median_x=sort_x.partition_point(|&(_,x)|x<=median_x);
+		let first_index_gt_median_y=sort_y.partition_point(|&(_,y)|y<=median_y);
+		let first_index_gt_median_z=sort_z.partition_point(|&(_,z)|z<=median_z);
+		//pick which side median value copies go into such that both sides are as balanced as possible based on distance from n/2
+		let partition_point_x=if n.abs_diff(2*first_index_eq_median_x)<n.abs_diff(2*first_index_gt_median_x){first_index_eq_median_x}else{first_index_gt_median_x};
+		let partition_point_y=if n.abs_diff(2*first_index_eq_median_y)<n.abs_diff(2*first_index_gt_median_y){first_index_eq_median_y}else{first_index_gt_median_y};
+		let partition_point_z=if n.abs_diff(2*first_index_eq_median_z)<n.abs_diff(2*first_index_gt_median_z){first_index_eq_median_z}else{first_index_gt_median_z};
+		//this ids which octant the boxen is put in
+		let mut octant=vec![0;n];
+		for &(i,_) in &sort_x[partition_point_x..]{
+			octant[i]+=1<<0;
+		}
+		for &(i,_) in &sort_y[partition_point_y..]{
+			octant[i]+=1<<1;
+		}
+		for &(i,_) in &sort_z[partition_point_z..]{
+			octant[i]+=1<<2;
+		}
+		//generate lists for unique octant values
+		let mut list_list=Vec::with_capacity(8);
+		let mut octant_list=Vec::with_capacity(8);
+		for (i,(data,aabb)) in boxen.into_iter().enumerate(){
+			let octant_id=octant[i];
+			let list_id=if let Some(list_id)=octant_list.iter().position(|&id|id==octant_id){
+				list_id
+			}else{
+				let list_id=list_list.len();
+				octant_list.push(octant_id);
+				list_list.push(Vec::new());
+				list_id
+			};
+			list_list[list_id].push((data,aabb));
+		}
+		let mut aabb=Aabb::default();
+		if list_list.len()==1{
+			generate_bvh_node(list_list.remove(0),true)
+		}else{
+			BvhNode{
+				content:RecursiveContent::Branch(
+					list_list.into_iter().map(|b|{
+						let node=generate_bvh_node(b,false);
+						aabb.join(&node.aabb);
+						node
+					}).collect()
+				),
+				aabb,
+			}
+		}
+	}
+}
diff --git a/lib/common/src/controls_bitflag.rs b/lib/common/src/controls_bitflag.rs
new file mode 100644
index 0000000..f7e7def
--- /dev/null
+++ b/lib/common/src/controls_bitflag.rs
@@ -0,0 +1,30 @@
+bitflags::bitflags!{
+	#[derive(Clone,Copy,Debug,Default)]
+	pub struct Controls:u32{
+		const MoveForward=1<<0;
+		const MoveLeft=1<<1;
+		const MoveBackward=1<<2;
+		const MoveRight=1<<3;
+		const MoveUp=1<<4;
+		const MoveDown=1<<5;
+		const LookUp=1<<6;
+		const LookLeft=1<<7;
+		const LookDown=1<<8;
+		const LookRight=1<<9;
+		const Jump=1<<10;
+		const Crouch=1<<11;
+		const Sprint=1<<12;
+		const Zoom=1<<13;
+		const Use=1<<14;//Interact with object
+		const PrimaryAction=1<<15;//LBM/Shoot/Melee
+		const SecondaryAction=1<<16;//RMB/ADS/Block
+	}
+}
+impl Controls{
+	pub const fn wasd()->Self{
+		Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight)
+	}
+	pub const fn wasdqe()->Self{
+		Self::MoveForward.union(Self::MoveLeft).union(Self::MoveBackward).union(Self::MoveRight).union(Self::MoveUp).union(Self::MoveDown)
+	}
+}
\ No newline at end of file
diff --git a/lib/common/src/gameplay_attributes.rs b/lib/common/src/gameplay_attributes.rs
new file mode 100644
index 0000000..ff01731
--- /dev/null
+++ b/lib/common/src/gameplay_attributes.rs
@@ -0,0 +1,174 @@
+use crate::model;
+use crate::integer::{Time,Planar64,Planar64Vec3};
+
+//you have this effect while in contact
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub struct ContactingLadder{
+	pub sticky:bool
+}
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub enum ContactingBehaviour{
+	Surf,
+	Ladder(ContactingLadder),
+	NoJump,
+	Cling,//usable as a zipline, or other weird and wonderful things
+	Elastic(u32),//[1/2^32,1] 0=None (elasticity+1)/2^32
+}
+//you have this effect while intersecting
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub struct IntersectingWater{
+	pub viscosity:Planar64,
+	pub density:Planar64,
+	pub velocity:Planar64Vec3,
+}
+//All models can be given these attributes
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub struct Accelerator{
+	pub acceleration:Planar64Vec3
+}
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub enum Booster{
+	//Affine(crate::integer::Planar64Affine3),//capable of SetVelocity,DotVelocity,normal booster,bouncy part,redirect velocity, and much more
+	Velocity(Planar64Vec3),//straight up boost velocity adds to your current velocity
+	Energy{direction:Planar64Vec3,energy:Planar64},//increase energy in direction
+	AirTime(Time),//increase airtime, invariant across mass and gravity changes
+	Height(Planar64),//increase height, invariant across mass and gravity changes
+}
+impl Booster{
+	pub fn boost(&self,velocity:Planar64Vec3)->Planar64Vec3{
+		match self{
+			&Booster::Velocity(boost_velocity)=>velocity+boost_velocity,
+			&Booster::Energy{..}=>{
+				todo!()
+				//let d=direction.dot(velocity);
+				//TODO: think about negative
+				//velocity+direction.with_length((d*d+energy).sqrt()-d)
+			},
+			Booster::AirTime(_)=>todo!(),
+			Booster::Height(_)=>todo!(),
+		}
+	}
+}
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub enum TrajectoryChoice{
+	HighArcLongDuration,//underhand lob at target: less horizontal speed and more air time
+	LowArcShortDuration,//overhand throw at target: more horizontal speed and less air time
+}
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub enum SetTrajectory{
+	//Speed-type SetTrajectory
+	AirTime(Time),//air time (relative to gravity direction) is invariant across mass and gravity changes
+	Height(Planar64),//boost height (relative to gravity direction) is invariant across mass and gravity changes
+	DotVelocity{direction:Planar64Vec3,dot:Planar64},//set your velocity in a specific direction without touching other directions
+	//Velocity-type SetTrajectory
+	TargetPointTime{//launch on a trajectory that will land at a target point in a set amount of time
+		target_point:Planar64Vec3,
+		time:Time,//short time = fast and direct, long time = launch high in the air, negative time = wrong way
+	},
+	TargetPointSpeed{//launch at a fixed speed and land at a target point
+		target_point:Planar64Vec3,
+		speed:Planar64,//if speed is too low this will fail to reach the target.  The closest-passing trajectory will be chosen instead
+		trajectory_choice:TrajectoryChoice,
+	},
+	Velocity(Planar64Vec3),//SetVelocity
+}
+impl SetTrajectory{
+	pub const fn is_absolute(&self)->bool{
+		match self{
+			SetTrajectory::AirTime(_)
+			|SetTrajectory::Height(_)
+			|SetTrajectory::DotVelocity{direction:_,dot:_}=>false,
+			SetTrajectory::TargetPointTime{target_point:_,time:_}
+			|SetTrajectory::TargetPointSpeed{target_point:_,speed:_,trajectory_choice:_}
+			|SetTrajectory::Velocity(_)=>true,
+		}
+	}
+}
+// enum TrapCondition{
+// 	FasterThan(Planar64),
+// 	SlowerThan(Planar64),
+// 	InRange(Planar64,Planar64),
+// 	OutsideRange(Planar64,Planar64),
+// }
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub struct Wormhole{
+	//destination does not need to be another wormhole
+	//this defines a one way portal to a destination model transform
+	//two of these can create a two way wormhole
+	pub destination_model:model::ModelId,
+	//(position,angles)*=origin.transform.inverse()*destination.transform
+}
+//attributes listed in order of handling
+#[derive(Default,Clone,Hash,Eq,PartialEq)]
+pub struct GeneralAttributes{
+	pub booster:Option<Booster>,
+	pub trajectory:Option<SetTrajectory>,
+	pub wormhole:Option<Wormhole>,
+	pub accelerator:Option<Accelerator>,
+}
+impl GeneralAttributes{
+	pub const fn any(&self)->bool{
+		self.booster.is_some()
+		||self.trajectory.is_some()
+		||self.wormhole.is_some()
+		||self.accelerator.is_some()
+	}
+	pub fn is_wrcp(&self)->bool{
+		self.trajectory.as_ref().map_or(false,|t|t.is_absolute())
+		/*
+		&&match &self.teleport_behaviour{
+			Some(TeleportBehaviour::StageElement(
+				StageElement{
+					mode_id,
+					stage_id:_,
+					force:true,
+					behaviour:StageElementBehaviour::Trigger|StageElementBehaviour::Teleport
+				}
+			))=>current_mode_id==*mode_id,
+			_=>false,
+		}
+		*/
+	}
+}
+#[derive(Default,Clone,Hash,Eq,PartialEq)]
+pub struct ContactingAttributes{
+	//friction?
+	pub contact_behaviour:Option<ContactingBehaviour>,
+}
+impl ContactingAttributes{
+	pub const fn any(&self)->bool{
+		self.contact_behaviour.is_some()
+	}
+}
+#[derive(Default,Clone,Hash,Eq,PartialEq)]
+pub struct IntersectingAttributes{
+	pub water:Option<IntersectingWater>,
+}
+impl IntersectingAttributes{
+	pub const fn any(&self)->bool{
+		self.water.is_some()
+	}
+}
+#[derive(Clone,Copy,id::Id,Hash,Eq,PartialEq)]
+pub struct CollisionAttributesId(u32);
+#[derive(Clone,Default,Hash,Eq,PartialEq)]
+pub struct ContactAttributes{
+	pub contacting:ContactingAttributes,
+	pub general:GeneralAttributes,
+}
+#[derive(Clone,Default,Hash,Eq,PartialEq)]
+pub struct IntersectAttributes{
+	pub intersecting:IntersectingAttributes,
+	pub general:GeneralAttributes,
+}
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub enum CollisionAttributes{
+	Decoration,//visual only
+	Contact(ContactAttributes),//track whether you are contacting the object
+	Intersect(IntersectAttributes),//track whether you are intersecting the object
+}
+impl CollisionAttributes{
+	pub fn contact_default()->Self{
+		Self::Contact(ContactAttributes::default())
+	}
+}
diff --git a/lib/common/src/gameplay_modes.rs b/lib/common/src/gameplay_modes.rs
new file mode 100644
index 0000000..b35acc6
--- /dev/null
+++ b/lib/common/src/gameplay_modes.rs
@@ -0,0 +1,331 @@
+use std::collections::{HashSet,HashMap};
+use crate::model::ModelId;
+use crate::gameplay_style;
+use crate::updatable::Updatable;
+
+#[derive(Clone)]
+pub struct StageElement{
+	stage_id:StageId,//which stage spawn to send to
+	force:bool,//allow setting to lower spawn id i.e. 7->3
+	behaviour:StageElementBehaviour,
+	jump_limit:Option<u8>,
+}
+impl StageElement{
+	#[inline]
+	pub const fn new(stage_id:StageId,force:bool,behaviour:StageElementBehaviour,jump_limit:Option<u8>)->Self{
+		Self{
+			stage_id,
+			force,
+			behaviour,
+			jump_limit,
+		}
+	}
+	#[inline]
+	pub const fn stage_id(&self)->StageId{
+		self.stage_id
+	}
+	#[inline]
+	pub const fn force(&self)->bool{
+		self.force
+	}
+	#[inline]
+	pub const fn behaviour(&self)->StageElementBehaviour{
+		self.behaviour
+	}
+	#[inline]
+	pub const fn jump_limit(&self)->Option<u8>{
+		self.jump_limit
+	}
+}
+
+#[derive(Clone,Copy,Hash,Eq,PartialEq)]
+pub enum StageElementBehaviour{
+	SpawnAt,//must be standing on top to get effect. except cancollide false
+	Trigger,
+	Teleport,
+	Platform,
+	//Check(point) acts like a trigger if you haven't hit all the checkpoints on previous stages yet.
+	//Note that all stage elements act like this, this is just the isolated behaviour.
+	Check,
+	Checkpoint,//this is a combined behaviour for Ordered & Unordered in case a model is used multiple times or for both.
+}
+
+#[derive(Clone,Copy,Debug,Hash,id::Id,Eq,PartialEq)]
+pub struct CheckpointId(u32);
+impl CheckpointId{
+	pub const FIRST:Self=Self(0);
+}
+#[derive(Clone,Copy,Debug,Hash,id::Id,Eq,PartialEq,Ord,PartialOrd)]
+pub struct StageId(u32);
+impl StageId{
+	pub const FIRST:Self=Self(0);
+}
+#[derive(Clone)]
+pub struct Stage{
+	spawn:ModelId,
+	//open world support lol
+	ordered_checkpoints_count:u32,
+	unordered_checkpoints_count:u32,
+	//currently loaded checkpoint models
+	ordered_checkpoints:HashMap<CheckpointId,ModelId>,
+	unordered_checkpoints:HashSet<ModelId>,
+}
+impl Stage{
+	pub fn new(
+		spawn:ModelId,
+		ordered_checkpoints_count:u32,
+		unordered_checkpoints_count:u32,
+		ordered_checkpoints:HashMap<CheckpointId,ModelId>,
+		unordered_checkpoints:HashSet<ModelId>,
+	)->Self{
+		Self{
+			spawn,
+			ordered_checkpoints_count,
+			unordered_checkpoints_count,
+			ordered_checkpoints,
+			unordered_checkpoints,
+		}
+	}
+	pub fn empty(spawn:ModelId)->Self{
+		Self{
+			spawn,
+			ordered_checkpoints_count:0,
+			unordered_checkpoints_count:0,
+			ordered_checkpoints:HashMap::new(),
+			unordered_checkpoints:HashSet::new(),
+		}
+	}
+	#[inline]
+	pub const fn spawn(&self)->ModelId{
+		self.spawn
+	}
+	#[inline]
+	pub const fn ordered_checkpoints_count(&self)->u32{
+		self.ordered_checkpoints_count
+	}
+	#[inline]
+	pub const fn unordered_checkpoints_count(&self)->u32{
+		self.unordered_checkpoints_count
+	}
+	pub fn into_inner(self)->(HashMap<CheckpointId,ModelId>,HashSet<ModelId>){
+		(self.ordered_checkpoints,self.unordered_checkpoints)
+	}
+	#[inline]
+	pub const fn is_empty(&self)->bool{
+		self.is_complete(0,0)
+	}
+	#[inline]
+	pub const fn is_complete(&self,ordered_checkpoints_count:u32,unordered_checkpoints_count:u32)->bool{
+		self.ordered_checkpoints_count==ordered_checkpoints_count&&self.unordered_checkpoints_count==unordered_checkpoints_count
+	}
+	#[inline]
+	pub fn is_next_ordered_checkpoint(&self,next_ordered_checkpoint_id:CheckpointId,model_id:ModelId)->bool{
+		self.ordered_checkpoints.get(&next_ordered_checkpoint_id).is_some_and(|&next_checkpoint|model_id==next_checkpoint)
+	}
+	#[inline]
+	pub fn is_unordered_checkpoint(&self,model_id:ModelId)->bool{
+		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{
+	Start,
+	Finish,
+	Anticheat,
+}
+#[derive(Clone,Copy,Debug,Hash,id::Id,Eq,PartialEq,Ord,PartialOrd)]
+pub struct ModeId(u32);
+impl ModeId{
+	pub const MAIN:Self=Self(0);
+	pub const BONUS:Self=Self(1);
+}
+#[derive(Clone)]
+pub struct Mode{
+	style:gameplay_style::StyleModifiers,
+	start:ModelId,//when you press reset you go here
+	zones:HashMap<ModelId,Zone>,
+	stages:Vec<Stage>,//when you load the map you go to stages[0].spawn
+	//mutually exlusive stage element behaviour
+	elements:HashMap<ModelId,StageElement>,
+}
+impl Mode{
+	pub fn new(
+		style:gameplay_style::StyleModifiers,
+		start:ModelId,
+		zones:HashMap<ModelId,Zone>,
+		stages:Vec<Stage>,
+		elements:HashMap<ModelId,StageElement>,
+	)->Self{
+		Self{
+			style,
+			start,
+			zones,
+			stages,
+			elements,
+		}
+	}
+	pub fn empty(style:gameplay_style::StyleModifiers,start:ModelId)->Self{
+		Self{
+			style,
+			start,
+			zones:HashMap::new(),
+			stages:Vec::new(),
+			elements:HashMap::new(),
+		}
+	}
+	pub fn into_inner(self)->(
+		gameplay_style::StyleModifiers,
+		ModelId,
+		HashMap<ModelId,Zone>,
+		Vec<Stage>,
+		HashMap<ModelId,StageElement>,
+	){
+		(
+			self.style,
+			self.start,
+			self.zones,
+			self.stages,
+			self.elements,
+		)
+	}
+	pub const fn get_start(&self)->ModelId{
+		self.start
+	}
+	pub const fn get_style(&self)->&gameplay_style::StyleModifiers{
+		&self.style
+	}
+	pub fn push_stage(&mut self,stage:Stage){
+		self.stages.push(stage)
+	}
+	pub fn get_stage_mut(&mut self,stage:StageId)->Option<&mut Stage>{
+		self.stages.get_mut(stage.0 as usize)
+	}
+	pub fn get_spawn_model_id(&self,stage:StageId)->Option<ModelId>{
+		self.stages.get(stage.0 as usize).map(|s|s.spawn)
+	}
+	pub fn get_zone(&self,model_id:ModelId)->Option<&Zone>{
+		self.zones.get(&model_id)
+	}
+	pub fn get_stage(&self,stage_id:StageId)->Option<&Stage>{
+		self.stages.get(stage_id.0 as usize)
+	}
+	pub fn get_element(&self,model_id:ModelId)->Option<&StageElement>{
+		self.elements.get(&model_id)
+	}
+	//TODO: put this in the SNF
+	pub fn denormalize_data(&mut self){
+		//expand and index normalized data
+		self.zones.insert(self.start,Zone::Start);
+		for (stage_id,stage) in self.stages.iter().enumerate(){
+			self.elements.insert(stage.spawn,StageElement{
+				stage_id:StageId(stage_id as u32),
+				force:false,
+				behaviour:StageElementBehaviour::SpawnAt,
+				jump_limit:None,
+			});
+			for (_,&model) in &stage.ordered_checkpoints{
+				self.elements.insert(model,StageElement{
+					stage_id:StageId(stage_id as u32),
+					force:false,
+					behaviour:StageElementBehaviour::Checkpoint,
+					jump_limit:None,
+				});
+			}
+			for &model in &stage.unordered_checkpoints{
+				self.elements.insert(model,StageElement{
+					stage_id:StageId(stage_id as u32),
+					force:false,
+					behaviour:StageElementBehaviour::Checkpoint,
+					jump_limit:None,
+				});
+			}
+		}
+	}
+}
+//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{
+	pub modes:Vec<Mode>,
+}
+impl Modes{
+	pub const fn new(modes:Vec<Mode>)->Self{
+		Self{
+			modes,
+		}
+	}
+	pub fn into_inner(self)->Vec<Mode>{
+		self.modes
+	}
+	pub fn push_mode(&mut self,mode:Mode){
+		self.modes.push(mode)
+	}
+	pub fn get_mode(&self,mode:ModeId)->Option<&Mode>{
+		self.modes.get(mode.0 as usize)
+	}
+}
+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){
+				mode.update(mode_update);
+			}
+		}
+	}
+}
diff --git a/lib/common/src/gameplay_style.rs b/lib/common/src/gameplay_style.rs
new file mode 100644
index 0000000..d56fa50
--- /dev/null
+++ b/lib/common/src/gameplay_style.rs
@@ -0,0 +1,611 @@
+const VALVE_SCALE:Planar64=Planar64::raw(1<<28);// 1/16
+
+use crate::integer::{int,vec3::int as int3,Time,Ratio64,Planar64,Planar64Vec3};
+use crate::controls_bitflag::Controls;
+
+#[derive(Clone,Debug)]
+pub struct StyleModifiers{
+	//controls which are allowed to pass into gameplay (usually all)
+	pub controls_mask:Controls,
+	//controls which are masked from control state (e.g. !jump in scroll style)
+	pub controls_mask_state:Controls,
+	//strafing
+	pub strafe:Option<StrafeSettings>,
+	//player gets a controllable rocket force
+	pub rocket:Option<PropulsionSettings>,
+	//flying
+	//pub move_type:MoveType::Fly(FlySettings)
+	//MoveType::Physics(PhysicsSettings) -> PhysicsSettings (strafe,rocket,jump,walk,ladder,swim,gravity)
+	//jumping is allowed
+	pub jump:Option<JumpSettings>,
+	//standing & walking is allowed
+	pub walk:Option<WalkSettings>,
+	//laddering is allowed
+	pub ladder:Option<LadderSettings>,
+	//water propulsion
+	pub swim:Option<PropulsionSettings>,
+	//maximum slope before sloped surfaces become frictionless
+	pub gravity:Planar64Vec3,
+	//hitbox
+	pub hitbox:Hitbox,
+	//camera location relative to the center (0,0,0) of the hitbox
+	pub camera_offset:Planar64Vec3,
+	//unused
+	pub mass:Planar64,
+}
+impl std::default::Default for StyleModifiers{
+	fn default()->Self{
+		Self::roblox_bhop()
+	}
+}
+
+#[derive(Clone,Debug)]
+pub enum JumpCalculation{
+	Max,//Roblox: jumped_speed=max(velocity.boost(),velocity.jump())
+	BoostThenJump,//jumped_speed=velocity.boost().jump()
+	JumpThenBoost,//jumped_speed=velocity.jump().boost()
+}
+
+#[derive(Clone,Debug)]
+pub enum JumpImpulse{
+	Time(Time),//jump time is invariant across mass and gravity changes
+	Height(Planar64),//jump height is invariant across mass and gravity changes
+	Linear(Planar64),//jump velocity is invariant across mass and gravity changes
+	Energy(Planar64),// :)
+}
+//Jumping acts on dot(walks_state.normal,body.velocity)
+//Energy means it adds energy
+//Linear means it linearly adds on
+impl JumpImpulse{
+	pub fn jump(
+		&self,
+		velocity:Planar64Vec3,
+		jump_dir:Planar64Vec3,
+		gravity:&Planar64Vec3,
+		mass:Planar64,
+	)->Planar64Vec3{
+		match self{
+			&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().fix_1()),
+			&JumpImpulse::Height(height)=>{
+				//height==-v.y*v.y/(2*g.y);
+				//use energy to determine max height
+				let gg=gravity.length_squared();
+				let g=gg.sqrt().fix_1();
+				let v_g=gravity.dot(velocity);
+				//do it backwards
+				let radicand=v_g*v_g+(g*height*2).fix_4();
+				velocity-(*gravity*(radicand.sqrt().fix_2()+v_g)/gg).divide().fix_1()
+			},
+			&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().fix_1(),
+			&JumpImpulse::Energy(energy)=>{
+				//calculate energy
+				//let e=gravity.dot(velocity);
+				//add
+				//you get the idea
+				todo!()
+			},
+		}
+	}
+	//TODO: remove this and implement JumpCalculation properly
+	pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
+		//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
+		match self{
+			&JumpImpulse::Time(time)=>(gravity.length().fix_1()*time/2).divide().fix_1(),
+			&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().fix_1(),
+			&JumpImpulse::Linear(deltav)=>deltav,
+			&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().fix_1(),
+		}
+	}
+}
+
+#[derive(Clone,Debug)]
+pub struct JumpSettings{
+	//information used to calculate jump power
+	pub impulse:JumpImpulse,
+	//information used to calculate jump behaviour
+	pub calculation:JumpCalculation,
+	//limit the minimum jump power when combined with downwards momentum
+	//This is true in both roblox and source
+	pub limit_minimum:bool,
+}
+impl JumpSettings{
+	pub fn jumped_velocity(
+		&self,
+		style:&StyleModifiers,
+		jump_dir:Planar64Vec3,
+		rel_velocity:Planar64Vec3,
+		booster:Option<&crate::gameplay_attributes::Booster>,
+	)->Planar64Vec3{
+		let jump_speed=self.impulse.get_jump_deltav(&style.gravity,style.mass);
+		match (self.limit_minimum,&self.calculation){
+			(true,JumpCalculation::Max)=>{
+				//the roblox calculation
+				let boost_vel=match booster{
+					Some(booster)=>booster.boost(rel_velocity),
+					None=>rel_velocity,
+				};
+				let j=boost_vel.dot(jump_dir);
+				let js=jump_speed.fix_2();
+				if j<js{
+					//weak booster: just do a regular jump
+					boost_vel+jump_dir.with_length(js-j).divide().fix_1()
+				}else{
+					//activate booster normally, jump does nothing
+					boost_vel
+				}
+			},
+			(true,_)=>{
+				//the source calculation (?)
+				let boost_vel=match booster{
+					Some(booster)=>booster.boost(rel_velocity),
+					None=>rel_velocity,
+				};
+				let j=boost_vel.dot(jump_dir);
+				let js=jump_speed.fix_2();
+				if j<js{
+					//speed in direction of jump cannot be lower than amount
+					boost_vel+jump_dir.with_length(js-j).divide().fix_1()
+				}else{
+					//boost and jump add together
+					boost_vel+jump_dir.with_length(js).divide().fix_1()
+				}
+			}
+			(false,JumpCalculation::Max)=>{
+				//??? calculation
+				//max(boost_vel,jump_vel)
+				let boost_vel=match booster{
+					Some(booster)=>booster.boost(rel_velocity),
+					None=>rel_velocity,
+				};
+				let boost_dot=boost_vel.dot(jump_dir);
+				let js=jump_speed.fix_2();
+				if boost_dot<js{
+					//weak boost is extended to jump speed
+					boost_vel+jump_dir.with_length(js-boost_dot).divide().fix_1()
+				}else{
+					//activate booster normally, jump does nothing
+					boost_vel
+				}
+			},
+			//the strafe client calculation
+			(false,_)=>{
+				let boost_vel=match booster{
+					Some(booster)=>booster.boost(rel_velocity),
+					None=>rel_velocity,
+				};
+				boost_vel+jump_dir.with_length(jump_speed).divide().fix_1()
+			},
+		}
+	}
+}
+
+#[derive(Clone,Debug)]
+pub struct ControlsActivation{
+	//allowed keys
+	pub controls_mask:Controls,
+	//allow strafing only if any of the masked controls are held, eg W|S for shsw
+	pub controls_intersects:Controls,
+	//allow strafing only if all of the masked controls are held, eg W for hsw, w-only
+	pub controls_contains:Controls,
+	//Function(Box<dyn Fn(u32)->bool>),
+}
+impl ControlsActivation{
+	pub const fn mask(&self,controls:Controls)->Controls{
+		controls.intersection(self.controls_mask)
+	}
+	pub const fn activates(&self,controls:Controls)->bool{
+		(self.controls_intersects.is_empty()||controls.intersects(self.controls_intersects))
+		&&controls.contains(self.controls_contains)
+	}
+	pub const fn full_3d()->Self{
+		Self{
+			controls_mask:Controls::wasdqe(),
+			controls_intersects:Controls::wasdqe(),
+			controls_contains:Controls::empty(),
+		}
+	}
+	//classical styles
+	//Normal
+	pub const fn full_2d()->Self{
+		Self{
+			controls_mask:Controls::wasd(),
+			controls_intersects:Controls::wasd(),
+			controls_contains:Controls::empty(),
+		}
+	}
+	//Sideways
+	pub const fn sideways()->Self{
+		Self{
+			controls_mask:Controls::MoveForward.union(Controls::MoveBackward),
+			controls_intersects:Controls::MoveForward.union(Controls::MoveBackward),
+			controls_contains:Controls::empty(),
+		}
+	}
+	//Half-Sideways
+	pub const fn half_sideways()->Self{
+		Self{
+			controls_mask:Controls::MoveForward.union(Controls::MoveLeft).union(Controls::MoveRight),
+			controls_intersects:Controls::MoveLeft.union(Controls::MoveRight),
+			controls_contains:Controls::MoveForward,
+		}
+	}
+	//Surf Half-Sideways
+	pub const fn surf_half_sideways()->Self{
+		Self{
+			controls_mask:Controls::MoveForward.union(Controls::MoveBackward).union(Controls::MoveLeft).union(Controls::MoveRight),
+			controls_intersects:Controls::MoveForward.union(Controls::MoveBackward),
+			controls_contains:Controls::empty(),
+		}
+	}
+	//W-Only
+	pub const fn w_only()->Self{
+		Self{
+			controls_mask:Controls::MoveForward,
+			controls_intersects:Controls::empty(),
+			controls_contains:Controls::MoveForward,
+		}
+	}
+	//A-Only
+	pub const fn a_only()->Self{
+		Self{
+			controls_mask:Controls::MoveLeft,
+			controls_intersects:Controls::empty(),
+			controls_contains:Controls::MoveLeft,
+		}
+	}
+	//Backwards
+}
+
+#[derive(Clone,Debug)]
+pub struct StrafeSettings{
+	pub enable:ControlsActivation,
+	pub mv:Planar64,
+	pub air_accel_limit:Option<Planar64>,
+	pub tick_rate:Ratio64,
+}
+impl StrafeSettings{
+	pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
+		let d=velocity.dot(control_dir);
+		let mv=self.mv.fix_2();
+		match d<mv{
+			true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.fix_2().min(mv-d))).fix_1()),
+			false=>None,
+		}
+	}
+	pub fn next_tick(&self,time:Time)->Time{
+		Time::from_nanos(self.tick_rate.rhs_div_int(self.tick_rate.mul_int(time.nanos())+1))
+	}
+	pub const fn activates(&self,controls:Controls)->bool{
+		self.enable.activates(controls)
+	}
+	pub const fn mask(&self,controls:Controls)->Controls{
+		self.enable.mask(controls)
+	}
+}
+
+#[derive(Clone,Debug)]
+pub struct PropulsionSettings{
+	pub magnitude:Planar64,
+}
+impl PropulsionSettings{
+	pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
+		(control_dir*self.magnitude).fix_1()
+	}
+}
+
+#[derive(Clone,Debug)]
+pub struct AccelerateSettings{
+	pub accel:Planar64,
+	pub topspeed:Planar64,
+}
+#[derive(Clone,Debug)]
+pub struct WalkSettings{
+	pub accelerate:AccelerateSettings,
+	pub static_friction:Planar64,
+	pub kinetic_friction:Planar64,
+	//if a surf slope angle does not exist, then everything is slippery and walking is impossible
+	pub surf_dot:Planar64,//surf_dot<n.dot(up)/n.length()
+}
+impl WalkSettings{
+	pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
+		//TODO: fallible walk accel
+		let diff_len=target_diff.length().fix_1();
+		let friction=if diff_len<self.accelerate.topspeed{
+			self.static_friction
+		}else{
+			self.kinetic_friction
+		};
+		self.accelerate.accel.min((-gravity.y*friction).fix_1())
+	}
+	pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
+		if control_dir==crate::integer::vec3::ZERO{
+			return control_dir;
+		}
+		let nn=normal.length_squared();
+		let mm=control_dir.length_squared();
+		let nnmm=nn*mm;
+		let d=normal.dot(control_dir);
+		let dd=d*d;
+		if dd<nnmm{
+			let cr=normal.cross(control_dir);
+			if cr==crate::integer::vec3::ZERO_2{
+				crate::integer::vec3::ZERO
+			}else{
+				(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().fix_1()
+			}
+		}else{
+			crate::integer::vec3::ZERO
+		}
+	}
+	pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
+		//normal is not guaranteed to be unit length
+		let ny=normal.dot(up);
+		let h=normal.length().fix_1();
+		//remember this is a normal vector
+		ny.is_positive()&&h*self.surf_dot<ny
+	}
+}
+
+#[derive(Clone,Debug)]
+pub struct LadderSettings{
+	pub accelerate:AccelerateSettings,
+	//how close to pushing directly into/out of the ladder normal
+	//does your input need to be to redirect straight up/down the ladder
+	pub dot:Planar64,
+}
+impl LadderSettings{
+	pub const fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
+		//TODO: fallible ladder accel
+		self.accelerate.accel
+	}
+	pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
+		if control_dir==crate::integer::vec3::ZERO{
+			return control_dir;
+		}
+		let nn=normal.length_squared();
+		let mm=control_dir.length_squared();
+		let nnmm=nn*mm;
+		let d=normal.dot(control_dir);
+		let mut dd=d*d;
+		if (self.dot*self.dot*nnmm).fix_4()<dd{
+			if d.is_negative(){
+				control_dir=Planar64Vec3::new([Planar64::ZERO,mm.fix_1(),Planar64::ZERO]);
+			}else{
+				control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.fix_1(),Planar64::ZERO]);
+			}
+			dd=(normal.y*normal.y).fix_4();
+		}
+		//n=d if you are standing on top of a ladder and press E.
+		//two fixes:
+		//- ladder movement is not allowed on walkable surfaces
+		//- fix the underlying issue
+		if dd<nnmm{
+			let cr=normal.cross(control_dir);
+			if cr==crate::integer::vec3::ZERO_2{
+				crate::integer::vec3::ZERO
+			}else{
+				(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().fix_1()
+			}
+		}else{
+			crate::integer::vec3::ZERO
+		}
+	}
+}
+
+#[derive(Clone,Debug)]
+pub enum HitboxMesh{
+	Box,//source
+	Cylinder,//roblox
+	//Sphere,//roblox old physics
+	//Point,
+	//Line,
+	//DualCone,
+}
+
+#[derive(Clone,Debug)]
+pub struct Hitbox{
+	pub halfsize:Planar64Vec3,
+	pub mesh:HitboxMesh,
+}
+impl Hitbox{
+	pub fn roblox()->Self{
+		Self{
+			halfsize:int3(2,5,2)>>1,
+			mesh:HitboxMesh::Cylinder,
+		}
+	}
+	pub fn source()->Self{
+		Self{
+			halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).fix_1(),
+			mesh:HitboxMesh::Box,
+		}
+	}
+}
+
+impl StyleModifiers{
+	pub const RIGHT_DIR:Planar64Vec3=crate::integer::vec3::X;
+	pub const UP_DIR:Planar64Vec3=crate::integer::vec3::Y;
+	pub const FORWARD_DIR:Planar64Vec3=crate::integer::vec3::NEG_Z;
+
+	pub fn neo()->Self{
+		Self{
+			controls_mask:Controls::all(),
+			controls_mask_state:Controls::all(),
+			strafe:Some(StrafeSettings{
+				enable:ControlsActivation::full_2d(),
+				air_accel_limit:None,
+				mv:int(3),
+				tick_rate:Ratio64::new(64,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump:Some(JumpSettings{
+				impulse:JumpImpulse::Energy(int(512)),
+				calculation:JumpCalculation::JumpThenBoost,
+				limit_minimum:false,
+			}),
+			gravity:int3(0,-80,0),
+			mass:int(1),
+			rocket:None,
+			walk:Some(WalkSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(16),
+					accel:int(80),
+				},
+				static_friction:int(2),
+				kinetic_friction:int(3),//unrealistic: kinetic friction is typically lower than static
+				surf_dot:int(3)/4,
+			}),
+			ladder:Some(LadderSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(16),
+					accel:int(160),
+				},
+				dot:(int(1)/2).sqrt(),
+			}),
+			swim:Some(PropulsionSettings{
+				magnitude:int(12),
+			}),
+			hitbox:Hitbox::roblox(),
+			camera_offset:int3(0,2,0),//4.5-2.5=2
+		}
+	}
+
+	pub fn roblox_bhop()->Self{
+		Self{
+			controls_mask:Controls::all(),
+			controls_mask_state:Controls::all(),
+			strafe:Some(StrafeSettings{
+				enable:ControlsActivation::full_2d(),
+				air_accel_limit:None,
+				mv:int(27)/10,
+				tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump:Some(JumpSettings{
+				impulse:JumpImpulse::Time(Time::from_micros(715_588)),
+				calculation:JumpCalculation::Max,
+				limit_minimum:true,
+			}),
+			gravity:int3(0,-100,0),
+			mass:int(1),
+			rocket:None,
+			walk:Some(WalkSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(18),
+					accel:int(90),
+				},
+				static_friction:int(2),
+				kinetic_friction:int(3),//unrealistic: kinetic friction is typically lower than static
+				surf_dot:int(3)/4,// normal.y=0.75
+			}),
+			ladder:Some(LadderSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(18),
+					accel:int(180),
+				},
+				dot:(int(1)/2).sqrt(),
+			}),
+			swim:Some(PropulsionSettings{
+				magnitude:int(12),
+			}),
+			hitbox:Hitbox::roblox(),
+			camera_offset:int3(0,2,0),//4.5-2.5=2
+		}
+	}
+	pub fn roblox_surf()->Self{
+		Self{
+			gravity:int3(0,-50,0),
+			..Self::roblox_bhop()
+		}
+	}
+	pub fn roblox_rocket()->Self{
+		Self{
+			strafe:None,
+			rocket:Some(PropulsionSettings{
+				magnitude:int(200),
+			}),
+			..Self::roblox_bhop()
+		}
+	}
+
+	pub fn source_bhop()->Self{
+		Self{
+			controls_mask:Controls::all()-Controls::MoveUp-Controls::MoveDown,
+			controls_mask_state:Controls::all(),
+			strafe:Some(StrafeSettings{
+				enable:ControlsActivation::full_2d(),
+				air_accel_limit:Some(Planar64::raw(150<<28)*100),
+				mv:(Planar64::raw(30)*VALVE_SCALE).fix_1(),
+				tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump:Some(JumpSettings{
+				impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
+				calculation:JumpCalculation::JumpThenBoost,
+				limit_minimum:true,
+			}),
+			gravity:(int3(0,-800,0)*VALVE_SCALE).fix_1(),
+			mass:int(1),
+			rocket:None,
+			walk:Some(WalkSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(18),//?
+					accel:int(90),//?
+				},
+				static_friction:int(2),//?
+				kinetic_friction:int(3),//?
+				surf_dot:int(3)/4,// normal.y=0.75
+			}),
+			ladder:Some(LadderSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(18),//?
+					accel:int(180),//?
+				},
+				dot:(int(1)/2).sqrt(),//?
+			}),
+			swim:Some(PropulsionSettings{
+				magnitude:int(12),//?
+			}),
+			hitbox:Hitbox::source(),
+			camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).fix_1(),
+		}
+	}
+	pub fn source_surf()->Self{
+		Self{
+			controls_mask:Controls::all()-Controls::MoveUp-Controls::MoveDown,
+			controls_mask_state:Controls::all(),
+			strafe:Some(StrafeSettings{
+				enable:ControlsActivation::full_2d(),
+				air_accel_limit:Some((int(150)*66*VALVE_SCALE).fix_1()),
+				mv:(int(30)*VALVE_SCALE).fix_1(),
+				tick_rate:Ratio64::new(66,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump:Some(JumpSettings{
+				impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).fix_1()),
+				calculation:JumpCalculation::JumpThenBoost,
+				limit_minimum:true,
+			}),
+			gravity:(int3(0,-800,0)*VALVE_SCALE).fix_1(),
+			mass:int(1),
+			rocket:None,
+			walk:Some(WalkSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(18),//?
+					accel:int(90),//?
+				},
+				static_friction:int(2),//?
+				kinetic_friction:int(3),//?
+				surf_dot:int(3)/4,// normal.y=0.75
+			}),
+			ladder:Some(LadderSettings{
+				accelerate:AccelerateSettings{
+					topspeed:int(18),//?
+					accel:int(180),//?
+				},
+				dot:(int(1)/2).sqrt(),//?
+			}),
+			swim:Some(PropulsionSettings{
+				magnitude:int(12),//?
+			}),
+			hitbox:Hitbox::source(),
+			camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).fix_1(),
+		}
+	}
+}
diff --git a/lib/common/src/instruction.rs b/lib/common/src/instruction.rs
new file mode 100644
index 0000000..df07598
--- /dev/null
+++ b/lib/common/src/instruction.rs
@@ -0,0 +1,53 @@
+use crate::integer::Time;
+
+#[derive(Debug)]
+pub struct TimedInstruction<I>{
+	pub time:Time,
+	pub instruction:I,
+}
+
+pub trait InstructionEmitter<I>{
+	fn next_instruction(&self,time_limit:Time)->Option<TimedInstruction<I>>;
+}
+pub trait InstructionConsumer<I>{
+	fn process_instruction(&mut self, instruction:TimedInstruction<I>);
+}
+
+//PROPER PRIVATE FIELDS!!!
+pub struct InstructionCollector<I>{
+	time:Time,
+	instruction:Option<I>,
+}
+impl<I> InstructionCollector<I>{
+	pub const fn new(time:Time)->Self{
+		Self{
+			time,
+			instruction:None
+		}
+	}
+	#[inline]
+	pub const fn time(&self)->Time{
+		self.time
+	}
+	pub fn collect(&mut self,instruction:Option<TimedInstruction<I>>){
+		match instruction{
+			Some(unwrap_instruction)=>{
+				if unwrap_instruction.time<self.time {
+					self.time=unwrap_instruction.time;
+					self.instruction=Some(unwrap_instruction.instruction);
+				}
+			},
+			None=>(),
+		}
+	}
+	pub fn instruction(self)->Option<TimedInstruction<I>>{
+		//STEAL INSTRUCTION AND DESTROY INSTRUCTIONCOLLECTOR
+		match self.instruction{
+			Some(instruction)=>Some(TimedInstruction{
+				time:self.time,
+				instruction
+			}),
+			None=>None,
+		}
+	}
+}
diff --git a/lib/common/src/integer.rs b/lib/common/src/integer.rs
new file mode 100644
index 0000000..670102f
--- /dev/null
+++ b/lib/common/src/integer.rs
@@ -0,0 +1,664 @@
+pub use fixed_wide::fixed::{Fixed,Fix};
+pub use ratio_ops::ratio::{Ratio,Divide};
+
+//integer units
+#[derive(Clone,Copy,Hash,Eq,PartialEq,PartialOrd,Debug)]
+pub struct Time(i64);
+impl Time{
+	pub const MIN:Self=Self(i64::MIN);
+	pub const MAX:Self=Self(i64::MAX);
+	pub const ZERO:Self=Self(0);
+	pub const ONE_SECOND:Self=Self(1_000_000_000);
+	pub const ONE_MILLISECOND:Self=Self(1_000_000);
+	pub const ONE_MICROSECOND:Self=Self(1_000);
+	pub const ONE_NANOSECOND:Self=Self(1);
+	#[inline]
+	pub const fn raw(num:i64)->Self{
+		Self(num)
+	}
+	#[inline]
+	pub const fn get(self)->i64{
+		self.0
+	}
+	#[inline]
+	pub const fn from_secs(num:i64)->Self{
+		Self(Self::ONE_SECOND.0*num)
+	}
+	#[inline]
+	pub const fn from_millis(num:i64)->Self{
+		Self(Self::ONE_MILLISECOND.0*num)
+	}
+	#[inline]
+	pub const fn from_micros(num:i64)->Self{
+		Self(Self::ONE_MICROSECOND.0*num)
+	}
+	#[inline]
+	pub const fn from_nanos(num:i64)->Self{
+		Self(Self::ONE_NANOSECOND.0*num)
+	}
+	//should I have checked subtraction? force all time variables to be positive?
+	#[inline]
+	pub const fn nanos(self)->i64{
+		self.0
+	}
+	#[inline]
+	pub const fn to_ratio(self)->Ratio<Planar64,Planar64>{
+		Ratio::new(Planar64::raw(self.0),Planar64::raw(1_000_000_000))
+	}
+}
+impl From<Planar64> for Time{
+	#[inline]
+	fn from(value:Planar64)->Self{
+		Time((value*Planar64::raw(1_000_000_000)).fix_1().to_raw())
+	}
+}
+impl<Num,Den,N1,T1> From<Ratio<Num,Den>> for Time
+	where
+		Num:core::ops::Mul<Planar64,Output=N1>,
+		N1:Divide<Den,Output=T1>,
+		T1:Fix<Planar64>,
+{
+	#[inline]
+	fn from(value:Ratio<Num,Den>)->Self{
+		Time((value*Planar64::raw(1_000_000_000)).divide().fix().to_raw())
+	}
+}
+impl std::fmt::Display for Time{
+	#[inline]
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
+	}
+}
+impl std::default::Default for Time{
+	fn default()->Self{
+		Self(0)
+	}
+}
+impl std::ops::Neg for Time{
+	type Output=Time;
+	#[inline]
+	fn neg(self)->Self::Output {
+		Time(-self.0)
+	}
+}
+macro_rules! impl_time_additive_operator {
+	($trait:ty, $method:ident) => {
+		impl $trait for Time{
+			type Output=Time;
+			#[inline]
+			fn $method(self,rhs:Self)->Self::Output {
+				Time(self.0.$method(rhs.0))
+			}
+		}
+	};
+}
+impl_time_additive_operator!(core::ops::Add,add);
+impl_time_additive_operator!(core::ops::Sub,sub);
+impl_time_additive_operator!(core::ops::Rem,rem);
+macro_rules! impl_time_additive_assign_operator {
+	($trait:ty, $method:ident) => {
+		impl $trait for Time{
+			#[inline]
+			fn $method(&mut self,rhs:Self){
+				self.0.$method(rhs.0)
+			}
+		}
+	};
+}
+impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
+impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
+impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
+impl std::ops::Mul for Time{
+	type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
+	#[inline]
+	fn mul(self,rhs:Self)->Self::Output{
+		Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
+	}
+}
+impl std::ops::Div<i64> for Time{
+	type Output=Time;
+	#[inline]
+	fn div(self,rhs:i64)->Self::Output{
+		Time(self.0/rhs)
+	}
+}
+impl std::ops::Mul<i64> for Time{
+	type Output=Time;
+	#[inline]
+	fn mul(self,rhs:i64)->Self::Output{
+		Time(self.0*rhs)
+	}
+}
+impl core::ops::Mul<Time> for Planar64{
+	type Output=Ratio<Fixed<2,64>,Planar64>;
+	fn mul(self,rhs:Time)->Self::Output{
+		Ratio::new(self*Fixed::raw(rhs.0),Planar64::raw(1_000_000_000))
+	}
+}
+#[test]
+fn time_from_planar64(){
+	let a:Time=Planar64::from(1).into();
+	assert_eq!(a,Time::ONE_SECOND);
+}
+#[test]
+fn time_from_ratio(){
+	let a:Time=Ratio::new(Planar64::from(1),Planar64::from(1)).into();
+	assert_eq!(a,Time::ONE_SECOND);
+}
+#[test]
+fn time_squared(){
+	let a=Time::from_secs(2);
+	assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
+}
+#[test]
+fn time_times_planar64(){
+	let a=Time::from_secs(2);
+	let b=Planar64::from(2);
+	assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
+}
+
+#[inline]
+const fn gcd(mut a:u64,mut b:u64)->u64{
+	while b!=0{
+		(a,b)=(b,a.rem_euclid(b));
+	};
+	a
+}
+#[derive(Clone,Copy,Debug,Hash)]
+pub struct Ratio64{
+	num:i64,
+	den:u64,
+}
+impl Ratio64{
+	pub const ZERO:Self=Ratio64{num:0,den:1};
+	pub const ONE:Self=Ratio64{num:1,den:1};
+	#[inline]
+	pub const fn new(num:i64,den:u64)->Option<Ratio64>{
+		if den==0{
+			None
+		}else{
+			let d=gcd(num.unsigned_abs(),den);
+			Some(Self{num:num/(d as i64),den:den/d})
+		}
+	}
+	#[inline]
+	pub const fn num(self)->i64{
+		self.num
+	}
+	#[inline]
+	pub const fn den(self)->u64{
+		self.den
+	}
+	#[inline]
+	pub const fn mul_int(&self,rhs:i64)->i64{
+		rhs*self.num/(self.den as i64)
+	}
+	#[inline]
+	pub const fn rhs_div_int(&self,rhs:i64)->i64{
+		rhs*(self.den as i64)/self.num
+	}
+	#[inline]
+	pub const fn mul_ref(&self,rhs:&Ratio64)->Ratio64{
+		let (num,den)=(self.num*rhs.num,self.den*rhs.den);
+		let d=gcd(num.unsigned_abs(),den);
+		Self{
+			num:num/(d as i64),
+			den:den/d,
+		}
+	}
+}
+//from num_traits crate
+#[inline]
+fn integer_decode_f32(f: f32) -> (u64, i16, i8) {
+	let bits: u32 = f.to_bits();
+	let sign: i8 = if bits >> 31 == 0 { 1 } else { -1 };
+	let mut exponent: i16 = ((bits >> 23) & 0xff) as i16;
+	let mantissa = if exponent == 0 {
+		(bits & 0x7fffff) << 1
+	} else {
+		(bits & 0x7fffff) | 0x800000
+	};
+	// Exponent bias + mantissa shift
+	exponent -= 127 + 23;
+	(mantissa as u64, exponent, sign)
+}
+#[inline]
+fn integer_decode_f64(f: f64) -> (u64, i16, i8) {
+	let bits: u64 = f.to_bits();
+	let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 };
+	let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16;
+	let mantissa = if exponent == 0 {
+		(bits & 0xfffffffffffff) << 1
+	} else {
+		(bits & 0xfffffffffffff) | 0x10000000000000
+	};
+	// Exponent bias + mantissa shift
+	exponent -= 1023 + 52;
+	(mantissa, exponent, sign)
+}
+#[derive(Debug)]
+pub enum Ratio64TryFromFloatError{
+	Nan,
+	Infinite,
+	Subnormal,
+	HighlyNegativeExponent(i16),
+	HighlyPositiveExponent(i16),
+}
+const MAX_DENOMINATOR:u128=u64::MAX as u128;
+#[inline]
+fn ratio64_from_mes((m,e,s):(u64,i16,i8))->Result<Ratio64,Ratio64TryFromFloatError>{
+	if e< -127{
+		//this can also just be zero
+		Err(Ratio64TryFromFloatError::HighlyNegativeExponent(e))
+	}else if e< -63{
+		//approximate input ratio within denominator limit
+		let mut target_num=m as u128;
+		let mut target_den=1u128<<-e;
+
+		let mut num=1;
+		let mut den=0;
+		let mut prev_num=0;
+		let mut prev_den=1;
+
+		while target_den!=0{
+			let whole=target_num/target_den;
+			(target_num,target_den)=(target_den,target_num-whole*target_den);
+			let new_num=whole*num+prev_num;
+			let new_den=whole*den+prev_den;
+			if MAX_DENOMINATOR<new_den{
+				break;
+			}else{
+				(prev_num,prev_den)=(num,den);
+				(num,den)=(new_num,new_den);
+			}
+		}
+
+		Ok(Ratio64::new(num as i64,den as u64).unwrap())
+	}else if e<0{
+		Ok(Ratio64::new((m as i64)*(s as i64),1<<-e).unwrap())
+	}else if (64-m.leading_zeros() as i16)+e<64{
+		Ok(Ratio64::new((m as i64)*(s as i64)*(1<<e),1).unwrap())
+	}else{
+		Err(Ratio64TryFromFloatError::HighlyPositiveExponent(e))
+	}
+}
+impl TryFrom<f32> for Ratio64{
+	type Error=Ratio64TryFromFloatError;
+	#[inline]
+	fn try_from(value:f32)->Result<Self,Self::Error>{
+		match value.classify(){
+			std::num::FpCategory::Nan=>Err(Self::Error::Nan),
+			std::num::FpCategory::Infinite=>Err(Self::Error::Infinite),
+			std::num::FpCategory::Zero=>Ok(Self::ZERO),
+			std::num::FpCategory::Subnormal
+			|std::num::FpCategory::Normal=>ratio64_from_mes(integer_decode_f32(value)),
+		}
+	}
+}
+impl TryFrom<f64> for Ratio64{
+	type Error=Ratio64TryFromFloatError;
+	#[inline]
+	fn try_from(value:f64)->Result<Self,Self::Error>{
+		match value.classify(){
+			std::num::FpCategory::Nan=>Err(Self::Error::Nan),
+			std::num::FpCategory::Infinite=>Err(Self::Error::Infinite),
+			std::num::FpCategory::Zero=>Ok(Self::ZERO),
+			std::num::FpCategory::Subnormal
+			|std::num::FpCategory::Normal=>ratio64_from_mes(integer_decode_f64(value)),
+		}
+	}
+}
+impl std::ops::Mul<Ratio64> for Ratio64{
+	type Output=Ratio64;
+	#[inline]
+	fn mul(self,rhs:Ratio64)->Self::Output{
+		let (num,den)=(self.num*rhs.num,self.den*rhs.den);
+		let d=gcd(num.unsigned_abs(),den);
+		Self{
+			num:num/(d as i64),
+			den:den/d,
+		}
+	}
+}
+impl std::ops::Mul<i64> for Ratio64{
+	type Output=Ratio64;
+	#[inline]
+	fn mul(self,rhs:i64)->Self::Output {
+		Self{
+			num:self.num*rhs,
+			den:self.den,
+		}
+	}
+}
+impl std::ops::Div<u64> for Ratio64{
+	type Output=Ratio64;
+	#[inline]
+	fn div(self,rhs:u64)->Self::Output {
+		Self{
+			num:self.num,
+			den:self.den*rhs,
+		}
+	}
+}
+#[derive(Clone,Copy,Debug,Hash)]
+pub struct Ratio64Vec2{
+	pub x:Ratio64,
+	pub y:Ratio64,
+}
+impl Ratio64Vec2{
+	pub const ONE:Self=Self{x:Ratio64::ONE,y:Ratio64::ONE};
+	#[inline]
+	pub const fn new(x:Ratio64,y:Ratio64)->Self{
+		Self{x,y}
+	}
+	#[inline]
+	pub const fn mul_int(&self,rhs:glam::I64Vec2)->glam::I64Vec2{
+		glam::i64vec2(
+			self.x.mul_int(rhs.x),
+			self.y.mul_int(rhs.y),
+		)
+	}
+}
+impl std::ops::Mul<i64> for Ratio64Vec2{
+	type Output=Ratio64Vec2;
+	#[inline]
+	fn mul(self,rhs:i64)->Self::Output {
+		Self{
+			x:self.x*rhs,
+			y:self.y*rhs,
+		}
+	}
+}
+
+///[-pi,pi) = [-2^31,2^31-1]
+#[derive(Clone,Copy,Hash)]
+pub struct Angle32(i32);
+impl Angle32{
+	const ANGLE32_TO_FLOAT64_RADIANS:f64=std::f64::consts::PI/((1i64<<31) as f64);
+	pub const FRAC_PI_2:Self=Self(1<<30);
+	pub const NEG_FRAC_PI_2:Self=Self(-1<<30);
+	pub const PI:Self=Self(-1<<31);
+	#[inline]
+	pub const fn wrap_from_i64(theta:i64)->Self{
+		//take lower bits
+		//note: this was checked on compiler explorer and compiles to 1 instruction!
+		Self(i32::from_ne_bytes(((theta&((1<<32)-1)) as u32).to_ne_bytes()))
+	}
+	#[inline]
+	pub fn clamp_from_i64(theta:i64)->Self{
+		//the assembly is a bit confusing for this, I thought it was checking the same thing twice
+		//but it's just checking and then overwriting the value for both upper and lower bounds.
+		Self(theta.clamp(i32::MIN as i64,i32::MAX as i64) as i32)
+	}
+	#[inline]
+	pub const fn get(&self)->i32{
+		self.0
+	}
+	/// Clamps the value towards the midpoint of the range.
+	/// Note that theta_min can be larger than theta_max and it will wrap clamp the other way around
+	#[inline]
+	pub fn clamp(&self,theta_min:Self,theta_max:Self)->Self{
+		//((max-min as u32)/2 as i32)+min
+		let midpoint=((
+			(theta_max.0 as u32)
+			.wrapping_sub(theta_min.0 as u32)
+			/2
+		) as i32)//(u32::MAX/2) as i32 ALWAYS works
+		.wrapping_add(theta_min.0);
+		//(theta-mid).clamp(max-mid,min-mid)+mid
+		Self(
+			self.0.wrapping_sub(midpoint)
+			.max(theta_min.0.wrapping_sub(midpoint))
+			.min(theta_max.0.wrapping_sub(midpoint))
+			.wrapping_add(midpoint)
+		)
+	}
+	#[inline]
+	pub fn cos_sin(&self)->(Planar64,Planar64){
+		/*
+		//cordic
+		let a=self.0 as u32;
+		//initialize based on the quadrant
+		let (mut x,mut y)=match (a&(1<<31)!=0,a&(1<<30)!=0){
+			(false,false)=>( 1i64<<32, 0i64    ),//TR
+			(false,true )=>( 0i64    , 1i64<<32),//TL
+			(true ,false)=>(-1i64<<32, 0i64    ),//BL
+			(true ,true )=>( 0i64    ,-1i64<<32),//BR
+		};
+		println!("x={} y={}",Planar64::raw(x),Planar64::raw(y));
+		for i in 0..30{
+			if a&(1<<(29-i))!=0{
+				(x,y)=(x-(y>>i),y+(x>>i));
+			}
+			println!("i={i} t={} x={} y={}",(a&(1<<(29-i))!=0) as u8,Planar64::raw(x),Planar64::raw(y));
+		}
+		//don't forget the gain
+		(Planar64::raw(x),Planar64::raw(y))
+		*/
+		let (s,c)=(self.0 as f64*Self::ANGLE32_TO_FLOAT64_RADIANS).sin_cos();
+		(Planar64::raw((c*((1u64<<32) as f64)) as i64),Planar64::raw((s*((1u64<<32) as f64)) as i64))
+	}
+}
+impl Into<f32> for Angle32{
+	#[inline]
+	fn into(self)->f32{
+		(self.0 as f64*Self::ANGLE32_TO_FLOAT64_RADIANS) as f32
+	}
+}
+impl std::ops::Neg for Angle32{
+	type Output=Angle32;
+	#[inline]
+	fn neg(self)->Self::Output{
+		Angle32(self.0.wrapping_neg())
+	}
+}
+impl std::ops::Add<Angle32> for Angle32{
+	type Output=Angle32;
+	#[inline]
+	fn add(self,rhs:Self)->Self::Output {
+		Angle32(self.0.wrapping_add(rhs.0))
+	}
+}
+impl std::ops::Sub<Angle32> for Angle32{
+	type Output=Angle32;
+	#[inline]
+	fn sub(self,rhs:Self)->Self::Output {
+		Angle32(self.0.wrapping_sub(rhs.0))
+	}
+}
+impl std::ops::Mul<i32> for Angle32{
+	type Output=Angle32;
+	#[inline]
+	fn mul(self,rhs:i32)->Self::Output {
+		Angle32(self.0.wrapping_mul(rhs))
+	}
+}
+impl std::ops::Mul<Angle32> for Angle32{
+	type Output=Angle32;
+	#[inline]
+	fn mul(self,rhs:Self)->Self::Output {
+		Angle32(self.0.wrapping_mul(rhs.0))
+	}
+}
+#[test]
+fn angle_sin_cos(){
+	fn close_enough(lhs:Planar64,rhs:Planar64)->bool{
+		(lhs-rhs).abs()<Planar64::EPSILON*4
+	}
+	fn test_angle(f:f64){
+		let a=Angle32((f/Angle32::ANGLE32_TO_FLOAT64_RADIANS) as i32);
+		println!("a={:#034b}",a.0);
+		let (c,s)=a.cos_sin();
+		let h=(s*s+c*c).sqrt();
+		println!("cordic s={} c={}",(s/h).divide(),(c/h).divide());
+		let (fs,fc)=f.sin_cos();
+		println!("float s={} c={}",fs,fc);
+		assert!(close_enough((c/h).divide().fix_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
+		assert!(close_enough((s/h).divide().fix_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
+	}
+	test_angle(1.0);
+	test_angle(std::f64::consts::PI/4.0);
+	test_angle(std::f64::consts::PI/8.0);
+}
+
+/* Unit type unused for now, may revive it for map files
+///[-1.0,1.0] = [-2^30,2^30]
+pub struct Unit32(i32);
+impl Unit32{
+	#[inline]
+	pub fn as_planar64(&self) -> Planar64{
+		Planar64(4*(self.0 as i64))
+	}
+}
+const UNIT32_ONE_FLOAT64=((1<<30) as f64);
+///[-1.0,1.0] = [-2^30,2^30]
+pub struct Unit32Vec3(glam::IVec3);
+impl TryFrom<[f32;3]> for Unit32Vec3{
+	type Error=Unit32TryFromFloatError;
+	fn try_from(value:[f32;3])->Result<Self,Self::Error>{
+		Ok(Self(glam::ivec3(
+			Unit32::try_from(Planar64::try_from(value[0])?)?.0,
+			Unit32::try_from(Planar64::try_from(value[1])?)?.0,
+			Unit32::try_from(Planar64::try_from(value[2])?)?.0,
+		)))
+	}
+}
+*/
+
+pub type Planar64TryFromFloatError=fixed_wide::fixed::FixedFromFloatError;
+pub type Planar64=fixed_wide::types::I32F32;
+pub type Planar64Vec3=linear_ops::types::Vector3<Planar64>;
+pub type Planar64Mat3=linear_ops::types::Matrix3<Planar64>;
+pub mod vec3{
+	use super::*;
+	pub use linear_ops::types::Vector3;
+	pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]);
+	pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
+	pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
+	pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
+	pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
+	pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
+	pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
+	pub const ONE:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ONE,Planar64::ONE]);
+	pub const NEG_X:Planar64Vec3=Planar64Vec3::new([Planar64::NEG_ONE,Planar64::ZERO,Planar64::ZERO]);
+	pub const NEG_Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::NEG_ONE,Planar64::ZERO]);
+	pub const NEG_Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::NEG_ONE]);
+	pub const NEG_ONE:Planar64Vec3=Planar64Vec3::new([Planar64::NEG_ONE,Planar64::NEG_ONE,Planar64::NEG_ONE]);
+	#[inline]
+	pub const fn int(x:i32,y:i32,z:i32)->Planar64Vec3{
+		Planar64Vec3::new([Planar64::raw((x as i64)<<32),Planar64::raw((y as i64)<<32),Planar64::raw((z as i64)<<32)])
+	}
+	#[inline]
+	pub fn raw_array(array:[i64;3])->Planar64Vec3{
+		Planar64Vec3::new(array.map(Planar64::raw))
+	}
+	#[inline]
+	pub fn raw_xyz(x:i64,y:i64,z:i64)->Planar64Vec3{
+		Planar64Vec3::new([Planar64::raw(x),Planar64::raw(y),Planar64::raw(z)])
+	}
+	#[inline]
+	pub fn try_from_f32_array([x,y,z]:[f32;3])->Result<Planar64Vec3,Planar64TryFromFloatError>{
+		Ok(Planar64Vec3::new([
+			try_from_f32(x)?,
+			try_from_f32(y)?,
+			try_from_f32(z)?,
+		]))
+	}
+}
+
+#[inline]
+pub fn int(value:i32)->Planar64{
+	Planar64::from(value)
+}
+#[inline]
+pub fn try_from_f32(value:f32)->Result<Planar64,Planar64TryFromFloatError>{
+	let result:Result<Planar64,_>=value.try_into();
+	match result{
+		Ok(ok)=>Ok(ok),
+		Err(e)=>e.underflow_to_zero(),
+	}
+}
+pub mod mat3{
+	use super::*;
+	pub use linear_ops::types::Matrix3;
+	#[inline]
+	pub const fn identity()->Planar64Mat3{
+		Planar64Mat3::new([
+			[Planar64::ONE,Planar64::ZERO,Planar64::ZERO],
+			[Planar64::ZERO,Planar64::ONE,Planar64::ZERO],
+			[Planar64::ZERO,Planar64::ZERO,Planar64::ONE],
+		])
+	}
+	#[inline]
+	pub fn from_diagonal(diag:Planar64Vec3)->Planar64Mat3{
+		Planar64Mat3::new([
+			[diag.x,Planar64::ZERO,Planar64::ZERO],
+			[Planar64::ZERO,diag.y,Planar64::ZERO],
+			[Planar64::ZERO,Planar64::ZERO,diag.z],
+		])
+	}
+	#[inline]
+	pub fn from_rotation_yx(x:Angle32,y:Angle32)->Planar64Mat3{
+		let (xc,xs)=x.cos_sin();
+		let (yc,ys)=y.cos_sin();
+		Planar64Mat3::from_cols([
+			Planar64Vec3::new([xc,Planar64::ZERO,-xs]),
+			Planar64Vec3::new([(xs*ys).fix_1(),yc,(xc*ys).fix_1()]),
+			Planar64Vec3::new([(xs*yc).fix_1(),-ys,(xc*yc).fix_1()]),
+		])
+	}
+	#[inline]
+	pub fn from_rotation_y(y:Angle32)->Planar64Mat3{
+		let (c,s)=y.cos_sin();
+		Planar64Mat3::from_cols([
+			Planar64Vec3::new([c,Planar64::ZERO,-s]),
+			vec3::Y,
+			Planar64Vec3::new([s,Planar64::ZERO,c]),
+		])
+	}
+	#[inline]
+	pub fn try_from_f32_array_2d([x_axis,y_axis,z_axis]:[[f32;3];3])->Result<Planar64Mat3,Planar64TryFromFloatError>{
+		Ok(Planar64Mat3::new([
+			vec3::try_from_f32_array(x_axis)?.to_array(),
+			vec3::try_from_f32_array(y_axis)?.to_array(),
+			vec3::try_from_f32_array(z_axis)?.to_array(),
+		]))
+	}
+}
+
+#[derive(Clone,Copy,Default,Hash,Eq,PartialEq)]
+pub struct Planar64Affine3{
+	pub matrix3:Planar64Mat3,//includes scale above 1
+	pub translation:Planar64Vec3,
+}
+impl Planar64Affine3{
+	#[inline]
+	pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
+		Self{matrix3,translation}
+	}
+	#[inline]
+	pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
+		self.translation.fix_2()+self.matrix3*point
+	}
+}
+impl Into<glam::Mat4> for Planar64Affine3{
+	#[inline]
+	fn into(self)->glam::Mat4{
+		let matrix3=self.matrix3.to_array().map(|row|row.map(Into::<f32>::into));
+		let translation=self.translation.to_array().map(Into::<f32>::into);
+		glam::Mat4::from_cols_array(&[
+			matrix3[0][0],matrix3[0][1],matrix3[0][2],0.0,
+			matrix3[1][0],matrix3[1][1],matrix3[1][2],0.0,
+			matrix3[2][0],matrix3[2][1],matrix3[2][2],0.0,
+			translation[0],translation[1],translation[2],1.0
+		])
+	}
+}
+
+#[test]
+fn test_sqrt(){
+	let r=int(400);
+	assert_eq!(r,Planar64::raw(1717986918400));
+	let s=r.sqrt();
+	assert_eq!(s,Planar64::raw(85899345920));
+}
diff --git a/lib/common/src/lib.rs b/lib/common/src/lib.rs
new file mode 100644
index 0000000..c80e88a
--- /dev/null
+++ b/lib/common/src/lib.rs
@@ -0,0 +1,15 @@
+pub mod bvh;
+pub mod map;
+pub mod run;
+pub mod aabb;
+pub mod model;
+pub mod mouse;
+pub mod timer;
+pub mod integer;
+pub mod physics;
+pub mod updatable;
+pub mod instruction;
+pub mod gameplay_attributes;
+pub mod gameplay_modes;
+pub mod gameplay_style;
+pub mod controls_bitflag;
diff --git a/lib/common/src/map.rs b/lib/common/src/map.rs
new file mode 100644
index 0000000..3c4020c
--- /dev/null
+++ b/lib/common/src/map.rs
@@ -0,0 +1,14 @@
+use crate::model;
+use crate::gameplay_modes;
+use crate::gameplay_attributes;
+//this is a temporary struct to try to get the code running again
+//TODO: use snf::map::Region to update the data in physics and graphics instead of this
+pub struct CompleteMap{
+	pub modes:gameplay_modes::Modes,
+	pub attributes:Vec<gameplay_attributes::CollisionAttributes>,
+	pub meshes:Vec<model::Mesh>,
+	pub models:Vec<model::Model>,
+	//RenderPattern
+	pub textures:Vec<Vec<u8>>,
+	pub render_configs:Vec<model::RenderConfig>,
+}
diff --git a/lib/common/src/model.rs b/lib/common/src/model.rs
new file mode 100644
index 0000000..3a9980b
--- /dev/null
+++ b/lib/common/src/model.rs
@@ -0,0 +1,133 @@
+use crate::integer::{Planar64Vec3,Planar64Affine3};
+use crate::gameplay_attributes;
+
+pub type TextureCoordinate=glam::Vec2;
+pub type Color4=glam::Vec4;
+#[derive(Clone,Copy,Hash,id::Id,PartialEq,Eq)]
+pub struct PositionId(u32);
+#[derive(Clone,Copy,Hash,id::Id,PartialEq,Eq)]
+pub struct TextureCoordinateId(u32);
+#[derive(Clone,Copy,Hash,id::Id,PartialEq,Eq)]
+pub struct NormalId(u32);
+#[derive(Clone,Copy,Hash,id::Id,PartialEq,Eq)]
+pub struct ColorId(u32);
+#[derive(Clone,Hash,PartialEq,Eq)]
+pub struct IndexedVertex{
+	pub pos:PositionId,
+	pub tex:TextureCoordinateId,
+	pub normal:NormalId,
+	pub color:ColorId,
+}
+#[derive(Clone,Copy,Hash,id::Id,PartialEq,Eq)]
+pub struct VertexId(u32);
+pub type IndexedVertexList=Vec<VertexId>;
+pub trait PolygonIter{
+	fn polys(&self)->impl Iterator<Item=&[VertexId]>;
+}
+pub trait MapVertexId{
+	fn map_vertex_id<F:Fn(VertexId)->VertexId>(self,f:F)->Self;
+}
+#[derive(Clone)]
+pub struct PolygonList(Vec<IndexedVertexList>);
+impl PolygonList{
+	pub const fn new(list:Vec<IndexedVertexList>)->Self{
+		Self(list)
+	}
+	pub fn extend<T:IntoIterator<Item=IndexedVertexList>>(&mut self,iter:T){
+		self.0.extend(iter);
+	}
+}
+impl PolygonIter for PolygonList{
+	fn polys(&self)->impl Iterator<Item=&[VertexId]>{
+		self.0.iter().map(|poly|poly.as_slice())
+	}
+}
+impl MapVertexId for PolygonList{
+	fn map_vertex_id<F:Fn(VertexId)->VertexId>(self,f:F)->Self{
+		Self(self.0.into_iter().map(|ivl|ivl.into_iter().map(&f).collect()).collect())
+	}
+}
+// pub struct TriangleStrip(IndexedVertexList);
+// impl PolygonIter for TriangleStrip{
+// 	fn polys(&self)->impl Iterator<Item=&[VertexId]>{
+// 		self.0.vertices.windows(3).enumerate().map(|(i,s)|if i&0!=0{return s.iter().rev()}else{return s.iter()})
+// 	}
+// }
+#[derive(Clone,Copy,Hash,id::Id,PartialEq,Eq)]
+pub struct PolygonGroupId(u32);
+#[derive(Clone)]
+pub enum PolygonGroup{
+	PolygonList(PolygonList),
+	//TriangleStrip(TriangleStrip),
+}
+impl PolygonIter for PolygonGroup{
+	fn polys(&self)->impl Iterator<Item=&[VertexId]>{
+		match self{
+			PolygonGroup::PolygonList(list)=>list.polys(),
+			//PolygonGroup::TriangleStrip(strip)=>strip.polys(),
+		}
+	}
+}
+impl MapVertexId for PolygonGroup{
+	fn map_vertex_id<F:Fn(VertexId)->VertexId>(self,f:F)->Self{
+		match self{
+			PolygonGroup::PolygonList(polys)=>Self::PolygonList(polys.map_vertex_id(f)),
+		}
+	}
+}
+/// Ah yes, a group of things to render at the same time
+#[derive(Clone,Copy,Debug,Hash,id::Id,Eq,PartialEq)]
+pub struct TextureId(u32);
+#[derive(Clone,Copy,Hash,id::Id,Eq,PartialEq)]
+pub struct RenderConfigId(u32);
+#[derive(Clone,Copy,Default)]
+pub struct RenderConfig{
+	pub texture:Option<TextureId>,
+}
+impl RenderConfig{
+	pub const fn texture(texture:TextureId)->Self{
+		Self{
+			texture:Some(texture),
+		}
+	}
+}
+#[derive(Clone)]
+pub struct IndexedGraphicsGroup{
+	//Render pattern material/texture/shader/flat color
+	pub render:RenderConfigId,
+	pub groups:Vec<PolygonGroupId>,
+}
+#[derive(Clone,Default)]
+pub struct IndexedPhysicsGroup{
+	//the polygons in this group are guaranteed to make a closed convex shape
+	pub groups:Vec<PolygonGroupId>,
+}
+//This is a superset of PhysicsModel and GraphicsModel
+#[derive(Clone,Copy,Debug,Hash,id::Id,Eq,PartialEq)]
+pub struct MeshId(u32);
+#[derive(Clone)]
+pub struct Mesh{
+	pub unique_pos:Vec<Planar64Vec3>,//Unit32Vec3
+	pub unique_normal:Vec<Planar64Vec3>,//Unit32Vec3
+	pub unique_tex:Vec<TextureCoordinate>,
+	pub unique_color:Vec<Color4>,
+	pub unique_vertices:Vec<IndexedVertex>,
+	//polygon groups are constant texture AND convexity slices
+	//note that this may need to be changed to be a list of individual faces
+	//for submeshes to work since face ids need to be consistent across submeshes
+	//so face == polygon_groups[face_id]
+	pub polygon_groups:Vec<PolygonGroup>,
+	//graphics indexed (by texture)
+	pub graphics_groups:Vec<IndexedGraphicsGroup>,
+	//physics indexed (by convexity)
+	pub physics_groups:Vec<IndexedPhysicsGroup>,
+}
+
+#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
+pub struct ModelId(u32);
+pub struct Model{
+	pub mesh:MeshId,
+	pub attributes:gameplay_attributes::CollisionAttributesId,
+	pub color:Color4,//transparency is in here
+	pub transform:Planar64Affine3,
+}
diff --git a/lib/common/src/mouse.rs b/lib/common/src/mouse.rs
new file mode 100644
index 0000000..c3795de
--- /dev/null
+++ b/lib/common/src/mouse.rs
@@ -0,0 +1,26 @@
+use crate::integer::Time;
+
+#[derive(Clone,Debug)]
+pub struct MouseState{
+	pub pos:glam::IVec2,
+	pub time:Time,
+}
+impl Default for MouseState{
+	fn default()->Self{
+		Self{
+			time:Time::ZERO,
+			pos:glam::IVec2::ZERO,
+		}
+	}
+}
+impl MouseState{
+	pub fn lerp(&self,target:&MouseState,time:Time)->glam::IVec2{
+		let m0=self.pos.as_i64vec2();
+		let m1=target.pos.as_i64vec2();
+		//these are deltas
+		let t1t=(target.time-time).nanos();
+		let tt0=(time-self.time).nanos();
+		let dt=(target.time-self.time).nanos();
+		((m0*t1t+m1*tt0)/dt).as_ivec2()
+	}
+}
diff --git a/lib/common/src/physics.rs b/lib/common/src/physics.rs
new file mode 100644
index 0000000..e3e7ac9
--- /dev/null
+++ b/lib/common/src/physics.rs
@@ -0,0 +1,27 @@
+#[derive(Clone,Debug)]
+pub enum Instruction{
+	ReplaceMouse(crate::mouse::MouseState,crate::mouse::MouseState),
+	SetNextMouse(crate::mouse::MouseState),
+	SetMoveRight(bool),
+	SetMoveUp(bool),
+	SetMoveBack(bool),
+	SetMoveLeft(bool),
+	SetMoveDown(bool),
+	SetMoveForward(bool),
+	SetJump(bool),
+	SetZoom(bool),
+	/// Reset: fully replace the physics state.
+	/// This forgets all inputs and settings which need to be reapplied.
+	Reset,
+	/// Restart: Teleport to the start zone.
+	Restart,
+	/// Spawn: Teleport to a specific mode's spawn
+	/// Sets current mode & spawn
+	Spawn(crate::gameplay_modes::ModeId,crate::gameplay_modes::StageId),
+	Idle,
+		//Idle: there were no input events, but the simulation is safe to advance to this timestep
+		//for interpolation / networking / playback reasons, most playback heads will always want
+		//to be 1 instruction ahead to generate the next state for interpolation.
+	PracticeFly,
+	SetSensitivity(crate::integer::Ratio64Vec2),
+}
diff --git a/lib/common/src/run.rs b/lib/common/src/run.rs
new file mode 100644
index 0000000..ae0836a
--- /dev/null
+++ b/lib/common/src/run.rs
@@ -0,0 +1,103 @@
+use crate::timer::{TimerFixed,Realtime,Paused,Unpaused};
+use crate::integer::Time;
+
+#[derive(Clone,Copy,Debug)]
+pub enum FlagReason{
+	Anticheat,
+	StyleChange,
+	Clock,
+	Pause,
+	Flying,
+	Gravity,
+	Timescale,
+	TimeTravel,
+	Teleport,
+}
+impl ToString for FlagReason{
+	fn to_string(&self)->String{
+		match self{
+			FlagReason::Anticheat=>"Passed through anticheat zone.",
+			FlagReason::StyleChange=>"Changed style.",
+			FlagReason::Clock=>"Incorrect clock.  (This can be caused by internet hiccups)",
+			FlagReason::Pause=>"Pausing is not allowed in this style.",
+			FlagReason::Flying=>"Flying is not allowed in this style.",
+			FlagReason::Gravity=>"Gravity modification is not allowed in this style.",
+			FlagReason::Timescale=>"Timescale is not allowed in this style.",
+			FlagReason::TimeTravel=>"Time travel is not allowed in this style.",
+			FlagReason::Teleport=>"Illegal teleport.",
+		}.to_owned()
+	}
+}
+
+#[derive(Debug)]
+pub enum Error{
+	NotStarted,
+	AlreadyStarted,
+	AlreadyFinished,
+}
+impl std::fmt::Display for Error{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		write!(f,"{self:?}")
+	}
+}
+impl std::error::Error for Error{}
+
+#[derive(Clone,Copy,Debug)]
+enum RunState{
+	Created,
+	Started{timer:TimerFixed<Realtime,Unpaused>},
+	Finished{timer:TimerFixed<Realtime,Paused>},
+}
+
+#[derive(Clone,Copy,Debug)]
+pub struct Run{
+	state:RunState,
+	flagged:Option<FlagReason>,
+}
+
+impl Run{
+	pub fn new()->Self{
+		Self{
+			state:RunState::Created,
+			flagged:None,
+		}
+	}
+	pub fn time(&self,time:Time)->Time{
+		match &self.state{
+			RunState::Created=>Time::ZERO,
+			RunState::Started{timer}=>timer.time(time),
+			RunState::Finished{timer}=>timer.time(time),
+		}
+	}
+	pub fn start(&mut self,time:Time)->Result<(),Error>{
+		match &self.state{
+			RunState::Created=>{
+				self.state=RunState::Started{
+					timer:TimerFixed::new(time,Time::ZERO),
+				};
+				Ok(())
+			},
+			RunState::Started{..}=>Err(Error::AlreadyStarted),
+			RunState::Finished{..}=>Err(Error::AlreadyFinished),
+		}
+	}
+	pub fn finish(&mut self,time:Time)->Result<(),Error>{
+		//this uses Copy
+		match &self.state{
+			RunState::Created=>Err(Error::NotStarted),
+			RunState::Started{timer}=>{
+				self.state=RunState::Finished{
+					timer:timer.into_paused(time),
+				};
+				Ok(())
+			},
+			RunState::Finished{..}=>Err(Error::AlreadyFinished),
+		}
+	}
+	pub fn flag(&mut self,flag_reason:FlagReason){
+		//don't replace the first reason the run was flagged
+		if self.flagged.is_none(){
+			self.flagged=Some(flag_reason);
+		}
+	}
+}
diff --git a/lib/common/src/timer.rs b/lib/common/src/timer.rs
new file mode 100644
index 0000000..59e558d
--- /dev/null
+++ b/lib/common/src/timer.rs
@@ -0,0 +1,319 @@
+use crate::integer::{Time,Ratio64};
+
+#[derive(Clone,Copy,Debug)]
+pub struct Paused;
+#[derive(Clone,Copy,Debug)]
+pub struct Unpaused;
+
+pub trait PauseState:Copy+std::fmt::Debug{
+	const IS_PAUSED:bool;
+	fn new()->Self;
+}
+impl PauseState for Paused{
+	const IS_PAUSED:bool=true;
+	fn new()->Self{
+		Self
+	}
+}
+impl PauseState for Unpaused{
+	const IS_PAUSED:bool=false;
+	fn new()->Self{
+		Self
+	}
+}
+
+#[derive(Clone,Copy,Debug)]
+pub struct Realtime{
+	offset:Time,
+}
+impl Realtime{
+	pub const fn new(offset:Time)->Self{
+		Self{offset}
+	}
+}
+
+#[derive(Clone,Copy,Debug)]
+pub struct Scaled{
+	scale:Ratio64,
+	offset:Time,
+}
+impl Scaled{
+	pub const fn new(scale:Ratio64,offset:Time)->Self{
+		Self{scale,offset}
+	}
+	const fn with_scale(scale:Ratio64)->Self{
+		Self{scale,offset:Time::ZERO}
+	}
+	const fn scale(&self,time:Time)->Time{
+		Time::raw(self.scale.mul_int(time.get()))
+	}
+	const fn get_scale(&self)->Ratio64{
+		self.scale
+	}
+	fn set_scale(&mut self,time:Time,new_scale:Ratio64){
+		let new_time=self.get_time(time);
+		self.scale=new_scale;
+		self.set_time(time,new_time);
+	}
+}
+
+pub trait TimerState:Copy+std::fmt::Debug{
+	fn identity()->Self;
+	fn get_time(&self,time:Time)->Time;
+	fn set_time(&mut self,time:Time,new_time:Time);
+	fn get_offset(&self)->Time;
+	fn set_offset(&mut self,offset:Time);
+}
+impl TimerState for Realtime{
+	fn identity()->Self{
+		Self{offset:Time::ZERO}
+	}
+	fn get_time(&self,time:Time)->Time{
+		time+self.offset
+	}
+	fn set_time(&mut self,time:Time,new_time:Time){
+		self.offset=new_time-time;
+	}
+	fn get_offset(&self)->Time{
+		self.offset
+	}
+	fn set_offset(&mut self,offset:Time){
+		self.offset=offset;
+	}
+}
+impl TimerState for Scaled{
+	fn identity()->Self{
+		Self{scale:Ratio64::ONE,offset:Time::ZERO}
+	}
+	fn get_time(&self,time:Time)->Time{
+		self.scale(time)+self.offset
+	}
+	fn set_time(&mut self,time:Time,new_time:Time){
+		self.offset=new_time-self.scale(time);
+	}
+	fn get_offset(&self)->Time{
+		self.offset
+	}
+	fn set_offset(&mut self,offset:Time){
+		self.offset=offset;
+	}
+}
+
+#[derive(Clone,Copy,Debug)]
+pub struct TimerFixed<T:TimerState,P:PauseState>{
+	state:T,
+	_paused:P,
+}
+
+//scaled timer methods are generic across PauseState
+impl<P:PauseState> TimerFixed<Scaled,P>{
+	pub fn scaled(time:Time,new_time:Time,scale:Ratio64)->Self{
+		let mut timer=Self{
+			state:Scaled::with_scale(scale),
+			_paused:P::new(),
+		};
+		timer.set_time(time,new_time);
+		timer
+	}
+	pub const fn get_scale(&self)->Ratio64{
+		self.state.get_scale()
+	}
+	pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
+		self.state.set_scale(time,new_scale)
+	}
+}
+
+//pause and unpause is generic across TimerState
+impl<T:TimerState> TimerFixed<T,Paused>{
+	pub fn into_unpaused(self,time:Time)->TimerFixed<T,Unpaused>{
+		let new_time=self.time(time);
+		let mut timer=TimerFixed{
+			state:self.state,
+			_paused:Unpaused,
+		};
+		timer.set_time(time,new_time);
+		timer
+	}
+}
+impl<T:TimerState> TimerFixed<T,Unpaused>{
+	pub fn into_paused(self,time:Time)->TimerFixed<T,Paused>{
+		let new_time=self.time(time);
+		let mut timer=TimerFixed{
+			state:self.state,
+			_paused:Paused,
+		};
+		timer.set_time(time,new_time);
+		timer
+	}
+}
+
+//the new constructor and time queries are generic across both
+impl<T:TimerState,P:PauseState> TimerFixed<T,P>{
+	pub fn new(time:Time,new_time:Time)->Self{
+		let mut timer=Self{
+			state:T::identity(),
+			_paused:P::new(),
+		};
+		timer.set_time(time,new_time);
+		timer
+	}
+	pub fn from_state(state:T)->Self{
+		Self{
+			state,
+			_paused:P::new(),
+		}
+	}
+	pub fn into_state(self)->T{
+		self.state
+	}
+	pub fn time(&self,time:Time)->Time{
+		match P::IS_PAUSED{
+			true=>self.state.get_offset(),
+			false=>self.state.get_time(time),
+		}
+	}
+	pub fn set_time(&mut self,time:Time,new_time:Time){
+		match P::IS_PAUSED{
+			true=>self.state.set_offset(new_time),
+			false=>self.state.set_time(time,new_time),
+		}
+	}
+}
+
+#[derive(Debug)]
+pub enum Error{
+	AlreadyPaused,
+	AlreadyUnpaused,
+}
+impl std::fmt::Display for Error{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		write!(f,"{self:?}")
+	}
+}
+impl std::error::Error for Error{}
+
+//wrapper type which holds type state internally
+#[derive(Clone,Debug)]
+pub enum Timer<T:TimerState>{
+	Paused(TimerFixed<T,Paused>),
+	Unpaused(TimerFixed<T,Unpaused>),
+}
+impl<T:TimerState> Timer<T>{
+	pub fn from_state(state:T,paused:bool)->Self{
+		match paused{
+			true=>Self::Paused(TimerFixed::from_state(state)),
+			false=>Self::Unpaused(TimerFixed::from_state(state)),
+		}
+	}
+	pub fn into_state(self)->(T,bool){
+		match self{
+			Self::Paused(timer)=>(timer.into_state(),true),
+			Self::Unpaused(timer)=>(timer.into_state(),false),
+		}
+	}
+	pub fn paused(time:Time,new_time:Time)->Self{
+		Self::Paused(TimerFixed::new(time,new_time))
+	}
+	pub fn unpaused(time:Time,new_time:Time)->Self{
+		Self::Unpaused(TimerFixed::new(time,new_time))
+	}
+	pub fn time(&self,time:Time)->Time{
+		match self{
+			Self::Paused(timer)=>timer.time(time),
+			Self::Unpaused(timer)=>timer.time(time),
+		}
+	}
+	pub fn set_time(&mut self,time:Time,new_time:Time){
+		match self{
+			Self::Paused(timer)=>timer.set_time(time,new_time),
+			Self::Unpaused(timer)=>timer.set_time(time,new_time),
+		}
+	}
+	pub fn pause(&mut self,time:Time)->Result<(),Error>{
+		*self=match *self{
+			Self::Paused(_)=>return Err(Error::AlreadyPaused),
+			Self::Unpaused(timer)=>Self::Paused(timer.into_paused(time)),
+		};
+		Ok(())
+	}
+	pub fn unpause(&mut self,time:Time)->Result<(),Error>{
+		*self=match *self{
+			Self::Paused(timer)=>Self::Unpaused(timer.into_unpaused(time)),
+			Self::Unpaused(_)=>return Err(Error::AlreadyUnpaused),
+		};
+		Ok(())
+	}
+	pub fn is_paused(&self)->bool{
+		match self{
+			Self::Paused(_)=>true,
+			Self::Unpaused(_)=>false,
+		}
+	}
+	pub fn set_paused(&mut self,time:Time,paused:bool)->Result<(),Error>{
+		match paused{
+			true=>self.pause(time),
+			false=>self.unpause(time),
+		}
+	}
+}
+//scaled timer methods are generic across PauseState
+impl Timer<Scaled>{
+	pub const fn get_scale(&self)->Ratio64{
+		match self{
+			Self::Paused(timer)=>timer.get_scale(),
+			Self::Unpaused(timer)=>timer.get_scale(),
+		}
+	}
+	pub fn set_scale(&mut self,time:Time,new_scale:Ratio64){
+		match self{
+			Self::Paused(timer)=>timer.set_scale(time,new_scale),
+			Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
+		}
+	}
+}
+
+#[cfg(test)]
+mod test{
+	use super::*;
+	macro_rules! sec {
+		($s: expr) => {
+			Time::from_secs($s)
+		};
+	}
+	#[test]
+	fn test_timerfixed_scaled(){
+		//create a paused timer that reads 0s
+		let timer=TimerFixed::<Scaled,Paused>::from_state(Scaled{scale:0.5f32.try_into().unwrap(),offset:sec!(0)});
+		//the paused timer at 1 second should read 0s
+		assert_eq!(timer.time(sec!(1)),sec!(0));
+
+		//unpause it after one second
+		let timer=timer.into_unpaused(sec!(1));
+		//the timer at 6 seconds should read 2.5s
+		assert_eq!(timer.time(sec!(6)),Time::from_millis(2500));
+
+		//pause the timer after 11 seconds
+		let timer=timer.into_paused(sec!(11));
+		//the paused timer at 20 seconds should read 5s
+		assert_eq!(timer.time(sec!(20)),sec!(5));
+	}
+	#[test]
+	fn test_timer()->Result<(),Error>{
+		//create a paused timer that reads 0s
+		let mut timer=Timer::<Realtime>::paused(sec!(0),sec!(0));
+		//the paused timer at 1 second should read 0s
+		assert_eq!(timer.time(sec!(1)),sec!(0));
+
+		//unpause it after one second
+		timer.unpause(sec!(1))?;
+		//the timer at 6 seconds should read 5s
+		assert_eq!(timer.time(sec!(6)),sec!(5));
+
+		//pause the timer after 11 seconds
+		timer.pause(sec!(11))?;
+		//the paused timer at 20 seconds should read 10s
+		assert_eq!(timer.time(sec!(20)),sec!(10));
+
+		Ok(())
+	}
+}
diff --git a/lib/common/src/updatable.rs b/lib/common/src/updatable.rs
new file mode 100644
index 0000000..13b6dd2
--- /dev/null
+++ b/lib/common/src/updatable.rs
@@ -0,0 +1,56 @@
+pub trait Updatable<Updater>{
+	fn update(&mut self,update:Updater);
+}
+#[derive(Clone,Copy,Hash,Eq,PartialEq)]
+struct InnerId(u32);
+#[derive(Clone)]
+struct Inner{
+	id:InnerId,
+	enabled:bool,
+}
+#[derive(Clone,Copy,Hash,Eq,PartialEq)]
+struct OuterId(u32);
+struct Outer{
+	id:OuterId,
+	inners:std::collections::HashMap<InnerId,Inner>,
+}
+
+enum Update<I,U>{
+	Insert(I),
+	Update(U),
+	Remove
+}
+
+struct InnerUpdate{
+	//#[updatable(Update)]
+	enabled:Option<bool>,
+}
+struct OuterUpdate{
+	//#[updatable(Insert,Update,Remove)]
+	inners:std::collections::HashMap<InnerId,Update<Inner,InnerUpdate>>,
+	//#[updatable(Update)]
+	//inners:std::collections::HashMap<InnerId,InnerUpdate>,
+}
+impl Updatable<InnerUpdate> for Inner{
+	fn update(&mut self,update:InnerUpdate){
+		if let Some(enabled)=update.enabled{
+			self.enabled=enabled;
+		}
+	}
+}
+impl Updatable<OuterUpdate> for Outer{
+	fn update(&mut self,update:OuterUpdate){
+		for (id,up) in update.inners{
+			match up{
+				Update::Insert(new_inner)=>self.inners.insert(id,new_inner),
+				Update::Update(inner_update)=>self.inners.get_mut(&id).map(|inner|{
+					let old=inner.clone();
+					inner.update(inner_update);
+					old
+				}),
+				Update::Remove=>self.inners.remove(&id),
+			};
+		}
+	}
+}
+//*/
\ No newline at end of file