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