From 7bfea4374661df67ff0fbe04933284dc1e2e5711 Mon Sep 17 00:00:00 2001
From: Quaternions <krakow20@gmail.com>
Date: Mon, 29 Jan 2024 22:38:43 -0800
Subject: [PATCH] wip

---
 src/gameplay_attributes.rs | 152 +++++++++++++++++++
 src/gameplay_modes.rs      |  90 ++++++++++++
 src/gameplay_style.rs      | 291 +++++++++++++++++++++++++++++++++++++
 src/lib.rs                 |   6 +-
 src/model.rs               |  54 +++++++
 5 files changed, 592 insertions(+), 1 deletion(-)
 create mode 100644 src/gameplay_attributes.rs
 create mode 100644 src/gameplay_modes.rs
 create mode 100644 src/gameplay_style.rs
 create mode 100644 src/model.rs

diff --git a/src/gameplay_attributes.rs b/src/gameplay_attributes.rs
new file mode 100644
index 0000000..1c493e2
--- /dev/null
+++ b/src/gameplay_attributes.rs
@@ -0,0 +1,152 @@
+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,
+	Cling,//usable as a zipline, or other weird and wonderful things
+	Ladder(ContactingLadder),
+	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
+}
+#[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{
+	fn is_velocity(&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_id:u32,
+	//(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 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,current_mode_id:u32)->bool{
+		self.trajectory.as_ref().map_or(false,|t|t.is_velocity())
+		/*
+		&&match &self.wormhole{
+			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 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 fn any(&self)->bool{
+		self.water.is_some()
+	}
+}
+pub struct CollisionAttributesId(u32);
+pub enum CollisionAttributes{
+	Decoration,//visual only
+	Contact{//track whether you are contacting the object
+		contacting:ContactingAttributes,
+		general:GeneralAttributes,
+	},
+	Intersect{//track whether you are intersecting the object
+		intersecting:IntersectingAttributes,
+		general:GeneralAttributes,
+	},
+}
+impl std::default::Default for CollisionAttributes{
+	fn default()->Self{
+		Self::Contact{
+			contacting:ContactingAttributes::default(),
+			general:GeneralAttributes::default()
+		}
+	}
+}
diff --git a/src/gameplay_modes.rs b/src/gameplay_modes.rs
new file mode 100644
index 0000000..a06fbb2
--- /dev/null
+++ b/src/gameplay_modes.rs
@@ -0,0 +1,90 @@
+use std::collections::{HashSet,HashMap};
+use crate::model::ModelId;
+use crate::gameplay_style;
+
+pub struct StageElement{
+	stage:StageId,//which stage spawn to send to
+	force:bool,//allow setting to lower spawn id i.e. 7->3
+	behaviour:StageElementBehaviour
+}
+
+#[derive(Clone,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.
+}
+
+pub struct StageId(u32);
+pub struct Stage{
+	spawn:ModelId,
+	//other behaviour models of this stage can have
+	ordered_checkpoints:Vec<ModelId>,
+	unordered_checkpoints:HashSet<ModelId>,
+}
+
+#[derive(Clone,Hash,Eq,PartialEq)]
+pub enum ZoneBehaviour{
+	Finish,
+	Anitcheat,
+}
+pub struct ModeId(u32);
+pub struct Mode{
+	style:gameplay_style::StyleModifiers,
+	start:ModelId,
+	zones:HashMap<ModelId,ZoneBehaviour>,
+	stages:Vec<Stage>,
+	//mutually exlusive stage element behaviour
+	elements:HashMap<ModelId,StageElement>,
+	jump_limit:HashMap<ModelId,u32>,
+}
+impl Mode{
+	pub fn get_spawn_model_id(&self,stage:StageId)->Option<ModelId>{
+		self.stages.get(stage.0 as usize).map(|s|s.spawn)
+	}
+	pub fn denormalize_data(&mut self){
+		//expand and index normalized data
+		for (stage_id,stage) in self.stages.iter().enumerate(){
+			self.elements.insert(stage.spawn,StageElement{
+				stage:StageId(stage_id as u32),
+				force:false,
+				behaviour:StageElementBehaviour::SpawnAt,
+			});
+			for &model_id in &stage.ordered_checkpoints{
+				self.elements.insert(model_id,StageElement{
+					stage:StageId(stage_id as u32),
+					force:false,
+					behaviour:StageElementBehaviour::Checkpoint,
+				});
+			}
+			for &model_id in &stage.unordered_checkpoints{
+				self.elements.insert(model_id,StageElement{
+					stage:StageId(stage_id as u32),
+					force:false,
+					behaviour:StageElementBehaviour::Checkpoint,
+				});
+			}
+		}
+	}
+}
+
+#[derive(Default)]
+pub struct Modes{
+	modes:Vec<Mode>,
+}
+impl Modes{
+	pub fn clear(&mut self){
+		self.modes.clear();
+	}
+	pub fn get_mode(&self,mode:ModeId)->Option<&Mode>{
+		self.modes.get(mode.0 as usize)
+	}
+	pub fn insert(&mut self,mode:Mode){
+		self.modes.push(mode);
+	}
+}
\ No newline at end of file
diff --git a/src/gameplay_style.rs b/src/gameplay_style.rs
new file mode 100644
index 0000000..279346c
--- /dev/null
+++ b/src/gameplay_style.rs
@@ -0,0 +1,291 @@
+use crate::aabb;
+use crate::integer::{self,Time,Ratio64,Planar64,Planar64Vec3,Planar64Mat3};
+
+pub struct StyleModifiers{
+	controls_used:u32,//controls which are allowed to pass into gameplay
+	controls_mask:u32,//controls which are masked from control state (e.g. jump in scroll style)
+	strafe:Option<StrafeSettings>,
+	jump_impulse:JumpImpulse,
+	jump_calculation:JumpCalculation,
+	static_friction:Planar64,
+	kinetic_friction:Planar64,
+	walk_speed:Planar64,
+	walk_accel:Planar64,
+	ladder_speed:Planar64,
+	ladder_accel:Planar64,
+	ladder_dot:Planar64,
+	swim_speed:Planar64,
+	mass:Planar64,
+	mv:Planar64,
+	surf_slope:Option<Planar64>,
+	rocket_force:Option<Planar64>,
+	gravity:Planar64Vec3,
+	hitbox:Hitbox,
+	camera_offset:Planar64Vec3,
+}
+impl std::default::Default for StyleModifiers{
+	fn default()->Self{
+		Self::roblox_bhop()
+	}
+}
+impl StyleModifiers{
+	const CONTROL_MOVEFORWARD:u32=0b00000001;
+	const CONTROL_MOVEBACK:u32=0b00000010;
+	const CONTROL_MOVERIGHT:u32=0b00000100;
+	const CONTROL_MOVELEFT:u32=0b00001000;
+	const CONTROL_MOVEUP:u32=0b00010000;
+	const CONTROL_MOVEDOWN:u32=0b00100000;
+	const CONTROL_JUMP:u32=0b01000000;
+	const CONTROL_ZOOM:u32=0b10000000;
+
+	const RIGHT_DIR:Planar64Vec3=Planar64Vec3::X;
+	const UP_DIR:Planar64Vec3=Planar64Vec3::Y;
+	const FORWARD_DIR:Planar64Vec3=Planar64Vec3::NEG_Z;
+
+	fn neo()->Self{
+		Self{
+			controls_used:!0,
+			controls_mask:!0,//&!(Self::CONTROL_MOVEUP|Self::CONTROL_MOVEDOWN),
+			strafe:Some(StrafeSettings{
+				enable:EnableStrafe::Always,
+				air_accel_limit:None,
+				tick_rate:Ratio64::new(64,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump_impulse:JumpImpulse::FromEnergy(Planar64::int(512)),
+			jump_calculation:JumpCalculation::Energy,
+			gravity:Planar64Vec3::int(0,-80,0),
+			static_friction:Planar64::int(2),
+			kinetic_friction:Planar64::int(3),//unrealistic: kinetic friction is typically lower than static
+			mass:Planar64::int(1),
+			mv:Planar64::int(3),
+			rocket_force:None,
+			walk_speed:Planar64::int(16),
+			walk_accel:Planar64::int(80),
+			ladder_speed:Planar64::int(16),
+			ladder_accel:Planar64::int(160),
+			ladder_dot:(Planar64::int(1)/2).sqrt(),
+			swim_speed:Planar64::int(12),
+			surf_slope:Some(Planar64::raw(7)/8),
+			hitbox:Hitbox::roblox(),
+			camera_offset:Planar64Vec3::int(0,2,0),//4.5-2.5=2
+		}
+	}
+
+	fn roblox_bhop()->Self{
+		Self{
+			controls_used:!0,
+			controls_mask:!0,//&!(Self::CONTROL_MOVEUP|Self::CONTROL_MOVEDOWN),
+			strafe:Some(StrafeSettings{
+				enable:EnableStrafe::Always,
+				air_accel_limit:None,
+				tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump_impulse:JumpImpulse::FromTime(Time::from_micros(715_588)),
+			jump_calculation:JumpCalculation::Capped,
+			gravity:Planar64Vec3::int(0,-100,0),
+			static_friction:Planar64::int(2),
+			kinetic_friction:Planar64::int(3),//unrealistic: kinetic friction is typically lower than static
+			mass:Planar64::int(1),
+			mv:Planar64::int(27)/10,
+			rocket_force:None,
+			walk_speed:Planar64::int(18),
+			walk_accel:Planar64::int(90),
+			ladder_speed:Planar64::int(18),
+			ladder_accel:Planar64::int(180),
+			ladder_dot:(Planar64::int(1)/2).sqrt(),
+			swim_speed:Planar64::int(12),
+			surf_slope:Some(Planar64::raw(3787805118)),// normal.y=0.75
+			hitbox:Hitbox::roblox(),
+			camera_offset:Planar64Vec3::int(0,2,0),//4.5-2.5=2
+		}
+	}
+	fn roblox_surf()->Self{
+		Self{
+			controls_used:!0,
+			controls_mask:!0,//&!(Self::CONTROL_MOVEUP|Self::CONTROL_MOVEDOWN),
+			strafe:Some(StrafeSettings{
+				enable:EnableStrafe::Always,
+				air_accel_limit:None,
+				tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump_impulse:JumpImpulse::FromTime(Time::from_micros(715_588)),
+			jump_calculation:JumpCalculation::Capped,
+			gravity:Planar64Vec3::int(0,-50,0),
+			static_friction:Planar64::int(2),
+			kinetic_friction:Planar64::int(3),//unrealistic: kinetic friction is typically lower than static
+			mass:Planar64::int(1),
+			mv:Planar64::int(27)/10,
+			rocket_force:None,
+			walk_speed:Planar64::int(18),
+			walk_accel:Planar64::int(90),
+			ladder_speed:Planar64::int(18),
+			ladder_accel:Planar64::int(180),
+			ladder_dot:(Planar64::int(1)/2).sqrt(),
+			swim_speed:Planar64::int(12),
+			surf_slope:Some(Planar64::raw(3787805118)),// normal.y=0.75
+			hitbox:Hitbox::roblox(),
+			camera_offset:Planar64Vec3::int(0,2,0),//4.5-2.5=2
+		}
+	}
+
+	fn source_bhop()->Self{
+		Self{
+			controls_used:!0,
+			controls_mask:!0,//&!(Self::CONTROL_MOVEUP|Self::CONTROL_MOVEDOWN),
+			strafe:Some(StrafeSettings{
+				enable:EnableStrafe::Always,
+				air_accel_limit:Some(Planar64::raw(150<<28)*100),
+				tick_rate:Ratio64::new(100,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump_impulse:JumpImpulse::FromHeight(Planar64::raw(52<<28)),
+			jump_calculation:JumpCalculation::Linear,
+			gravity:Planar64Vec3::raw(0,-800<<28,0),
+			static_friction:Planar64::int(2),//?
+			kinetic_friction:Planar64::int(3),//?
+			mass:Planar64::int(1),
+			mv:Planar64::raw(30<<28),
+			rocket_force:None,
+			walk_speed:Planar64::int(18),//?
+			walk_accel:Planar64::int(90),//?
+			ladder_speed:Planar64::int(18),//?
+			ladder_accel:Planar64::int(180),//?
+			ladder_dot:(Planar64::int(1)/2).sqrt(),//?
+			swim_speed:Planar64::int(12),//?
+			surf_slope:Some(Planar64::raw(3787805118)),// normal.y=0.75
+			hitbox:Hitbox::source(),
+			camera_offset:Planar64Vec3::raw(0,(64<<28)-(73<<27),0),
+		}
+	}
+	fn source_surf()->Self{
+		Self{
+			controls_used:!0,
+			controls_mask:!0,//&!(Self::CONTROL_MOVEUP|Self::CONTROL_MOVEDOWN),
+			strafe:Some(StrafeSettings{
+				enable:EnableStrafe::Always,
+				air_accel_limit:Some(Planar64::raw(150<<28)*66),
+				tick_rate:Ratio64::new(66,Time::ONE_SECOND.nanos() as u64).unwrap(),
+			}),
+			jump_impulse:JumpImpulse::FromHeight(Planar64::raw(52<<28)),
+			jump_calculation:JumpCalculation::Linear,
+			gravity:Planar64Vec3::raw(0,-800<<28,0),
+			static_friction:Planar64::int(2),//?
+			kinetic_friction:Planar64::int(3),//?
+			mass:Planar64::int(1),
+			mv:Planar64::raw(30<<28),
+			rocket_force:None,
+			walk_speed:Planar64::int(18),//?
+			walk_accel:Planar64::int(90),//?
+			ladder_speed:Planar64::int(18),//?
+			ladder_accel:Planar64::int(180),//?
+			ladder_dot:(Planar64::int(1)/2).sqrt(),//?
+			swim_speed:Planar64::int(12),//?
+			surf_slope:Some(Planar64::raw(3787805118)),// normal.y=0.75
+			hitbox:Hitbox::source(),
+			camera_offset:Planar64Vec3::raw(0,(64<<28)-(73<<27),0),
+		}
+	}
+	fn roblox_rocket()->Self{
+		Self{
+			controls_used:!0,
+			controls_mask:!0,
+			strafe:None,
+			jump_impulse:JumpImpulse::FromTime(Time::from_micros(715_588)),
+			jump_calculation:JumpCalculation::Capped,
+			gravity:Planar64Vec3::int(0,-100,0),
+			static_friction:Planar64::int(2),
+			kinetic_friction:Planar64::int(3),//unrealistic: kinetic friction is typically lower than static
+			mass:Planar64::int(1),
+			mv:Planar64::int(27)/10,
+			rocket_force:Some(Planar64::int(200)),
+			walk_speed:Planar64::int(18),
+			walk_accel:Planar64::int(90),
+			ladder_speed:Planar64::int(18),
+			ladder_accel:Planar64::int(180),
+			ladder_dot:(Planar64::int(1)/2).sqrt(),
+			swim_speed:Planar64::int(12),
+			surf_slope:Some(Planar64::raw(3787805118)),// normal.y=0.75
+			hitbox:Hitbox::roblox(),
+			camera_offset:Planar64Vec3::int(0,2,0),//4.5-2.5=2
+		}
+	}
+}
+
+enum JumpCalculation{
+	Capped,//roblox
+	Energy,//new
+	Linear,//source
+}
+
+enum JumpImpulse{
+	FromTime(Time),//jump time is invariant across mass and gravity changes
+	FromHeight(Planar64),//jump height is invariant across mass and gravity changes
+	FromDeltaV(Planar64),//jump velocity is invariant across mass and gravity changes
+	FromEnergy(Planar64),// :)
+}
+//Jumping acts on dot(walks_state.normal,body.velocity)
+//Capped means it increases the dot to the cap
+//Energy means it adds energy
+//Linear means it linearly adds on
+
+enum EnableStrafe{
+	Always,
+	MaskAny(u32),//hsw, shsw
+	MaskAll(u32),
+	//Function(Box<dyn Fn(u32)->bool>),
+}
+
+struct StrafeSettings{
+	enable:EnableStrafe,
+	air_accel_limit:Option<Planar64>,
+	tick_rate:Ratio64,
+}
+
+//Why have a dedicated type for hitbox? surely it can just be a TransformedMesh or something.
+struct Hitbox{
+	halfsize:Planar64Vec3,
+	mesh:PhysicsMesh,
+	transform:integer::Planar64Affine3,
+	normal_transform:Planar64Mat3,
+	transform_det:Planar64,
+}
+impl Hitbox{
+	fn new(mesh:PhysicsMesh,transform:integer::Planar64Affine3)->Self{
+		//calculate extents
+		let mut aabb=aabb::Aabb::default();
+		for vert in mesh.verts(){
+			aabb.grow(transform.transform_point3(vert));
+		}
+		Self{
+			halfsize:aabb.size()/2,
+			mesh,
+			transform,
+			normal_transform:transform.matrix3.inverse_times_det().transpose(),
+			transform_det:transform.matrix3.determinant(),
+		}
+	}
+	fn from_mesh_scale(mesh:PhysicsMesh,scale:Planar64Vec3)->Self{
+		let matrix3=Planar64Mat3::from_diagonal(scale);
+		Self{
+			halfsize:scale,
+			mesh,
+			normal_transform:matrix3.inverse_times_det().transpose(),
+			transform:integer::Planar64Affine3::new(matrix3,Planar64Vec3::ZERO),
+			transform_det:matrix3.determinant(),//scale.x*scale.y*scale.z but whatever
+		}
+	}
+	fn from_mesh_scale_offset(mesh:PhysicsMesh,scale:Planar64Vec3,offset:Planar64Vec3)->Self{
+		let matrix3=Planar64Mat3::from_diagonal(scale);
+		Self{
+			halfsize:scale,
+			mesh,
+			normal_transform:matrix3.inverse_times_det().transpose(),
+			transform:integer::Planar64Affine3::new(matrix3,offset),
+			transform_det:matrix3.determinant(),
+		}
+	}
+	#[inline]
+	fn transformed_mesh(&self)->TransformedMesh{
+		TransformedMesh::new(&self.mesh,&self.transform,&self.normal_transform,self.transform_det)
+	}
+}
+
diff --git a/src/lib.rs b/src/lib.rs
index 188abe1..99b060b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,9 @@
 pub mod bvh;
 pub mod aabb;
+pub mod model;
 pub mod zeroes;
 pub mod integer;
-pub mod instruction;
\ No newline at end of file
+pub mod instruction;
+pub mod gameplay_modes;
+pub mod gameplay_style;
+pub mod gameplay_attributes;
\ No newline at end of file
diff --git a/src/model.rs b/src/model.rs
new file mode 100644
index 0000000..cbf7370
--- /dev/null
+++ b/src/model.rs
@@ -0,0 +1,54 @@
+use crate::integer::{Planar64Vec3,Planar64Affine3};
+use crate::gameplay_attributes;
+
+pub type TextureCoordinate=glam::Vec2;
+pub type Color4=glam::Vec4;
+#[derive(Clone,Hash,PartialEq,Eq)]
+pub struct IndexedVertex{
+	pub pos:u32,
+	pub tex:u32,
+	pub normal:u32,
+	pub color:u32,
+}
+pub struct VertexId(u32);
+pub struct IndexedVertexList{
+	pub vertices:Vec<VertexId>,
+}
+pub struct GroupId(u32);
+pub enum IndexedGroup{
+	PolygonList(Vec<IndexedVertexList>),
+	//TriangleStrip(Vec<IndexedVertexList>),
+}
+pub struct RenderId(u32);
+pub struct IndexedGraphicsGroup{
+	//Render pattern material/texture/shader/flat color
+	pub render:RenderId,
+	pub groups:Vec<GroupId>,
+}
+pub struct IndexedPhysicsGroup{
+	//the polygons in this group are guaranteed to make a closed convex shape
+	pub groups:Vec<GroupId>,
+}
+//This is a superset of PhysicsModel and GraphicsModel
+pub struct IndexedModel{
+	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>,
+	//groups are constant texture AND convexity slices
+	pub groups:Vec<IndexedGroup>,
+	//graphics indexed (by texture)
+	pub graphics_sets:Vec<IndexedGraphicsGroup>,
+	//physics indexed (by convexity)
+	pub physics_sets:Vec<IndexedPhysicsGroup>,
+}
+
+#[derive(Clone,Copy,Hash,Eq,PartialEq)]
+pub struct ModelId(u32);
+pub struct Model{
+	pub model:ModelId,
+	pub attributes:gameplay_attributes::CollisionAttributesId,
+	pub color:Color4,//transparency is in here
+	pub transform:Planar64Affine3,
+}