diff --git a/lib/snf/.gitignore b/lib/snf/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/lib/snf/.gitignore @@ -0,0 +1 @@ +/target diff --git a/lib/snf/Cargo.lock b/lib/snf/Cargo.lock new file mode 100644 index 0000000..58d2c57 --- /dev/null +++ b/lib/snf/Cargo.lock @@ -0,0 +1,191 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "binrw" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f36b7cb3ab9ff6a2858650d8dc360e783a5d14dc29594db48c56a3c233cc265" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ea7a8c5c8eeffffac6d54d172444e15beffac6f817fac714460a9a9aa88da3" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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 = "bytemuck" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "fixed_wide" +version = "0.1.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "7a8d6e10c51c9df39ead915c62288afbc41d13e00368e526037e530ee5c58e13" +dependencies = [ + "arrayvec", + "bnum", + "paste", + "ratio_ops", +] + +[[package]] +name = "glam" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" + +[[package]] +name = "id" +version = "0.1.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "2337e7a6c273082b672e377e159d7a168fb51438461b7c4033c79a515dd7a25a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[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 = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[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.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +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.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "d8fcc44793ae84a1d80882f367980913292241c94eb87584de4010bdad4a918d" +dependencies = [ + "arrayvec", + "bitflags", + "fixed_wide", + "glam", + "id", + "linear_ops", + "ratio_ops", +] + +[[package]] +name = "strafesnet_snf" +version = "0.2.0" +dependencies = [ + "binrw", + "id", + "strafesnet_common", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/lib/snf/Cargo.toml b/lib/snf/Cargo.toml new file mode 100644 index 0000000..300777c --- /dev/null +++ b/lib/snf/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "strafesnet_snf" +version = "0.2.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +binrw = "0.14.0" +id = { version = "0.1.0", registry = "strafesnet" } +strafesnet_common = { version = "0.5.0", registry = "strafesnet" } diff --git a/lib/snf/LICENSE-APACHE b/lib/snf/LICENSE-APACHE new file mode 100644 index 0000000..a7e77cb --- /dev/null +++ b/lib/snf/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/snf/LICENSE-MIT b/lib/snf/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/lib/snf/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/snf/README.md b/lib/snf/README.md new file mode 100644 index 0000000..1d9e111 --- /dev/null +++ b/lib/snf/README.md @@ -0,0 +1,30 @@ +StrafesNET File Format +====================== + +## Example + +```rust +let file=std::fs::File::open(path)?; +let mut input=std::io::BufReader::new(file); +let snf=snf::read(input)?; +match snf{ + snf::SNF::Map(map)=>{}, + snf::SNF::Bot(bot)=>{}, + snf::SNF::Demo(demo)=>{}, +} +``` + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +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. + \ No newline at end of file diff --git a/lib/snf/src/bot.rs b/lib/snf/src/bot.rs new file mode 100644 index 0000000..ffdaee8 --- /dev/null +++ b/lib/snf/src/bot.rs @@ -0,0 +1,98 @@ +use binrw::{BinReaderExt, binrw}; + +#[derive(Debug)] +pub enum Error{ + InvalidHeader, + InvalidSegment(binrw::Error), + InvalidSegmentId(SegmentId), + File(crate::file::Error), +} + +/* block types + +BLOCK_BOT_HEADER: +u128 map_resource_uuid //which map is this bot running +//don't include style info in bot header because it's in the simulation state +//blocks are laid out in chronological order, but indices may jump around. +u64 num_segments +for _ in 0..num_segments{ + i64 time //simulation_state timestamp + u64 block_id +} + +BLOCK_BOT_SEGMENT: +//format version indicates what version of these structures to use +SimulationState simulation_state //SimulationState is just under ClientState which includes Play/Pause events that the simulation doesn't know about. +//to read, greedily decode instructions until eof +loop{ + //delta encode as much as possible (time,mousepos) + //strafe ticks are implied + //physics can be implied in an input-only bot file + TimedInstruction instruction +} + +*/ + +//error hiding mock code +mod simulation{ + #[super::binrw] + #[brw(little)] + pub struct State{} + #[super::binrw] + #[brw(little)] + pub struct Instruction{} +} +// mod instruction{ + // #[super::binrw] + // #[brw(little)] + // pub struct TimedInstruction{ + // time:u64, + // instruction:Instruction + // } +// } +// mod timeline{ + // #[super::binrw] + // #[brw(little)] + // pub struct Timeline{ + // #[bw(try_calc(u32::try_from(instructions.len())))] + // instruction_count:u32, + // #[br(count=instruction_count)] + // instructions:Vec> + // } +// } + +//serious code + +#[binrw] +#[brw(little)] +#[derive(Clone,Copy,Debug,id::Id)] +pub struct SegmentId(u32); + +#[binrw] +#[brw(little)] +pub struct Segment{ + state:simulation::State, + //#[bw(try_calc(u32::try_from(instructions.len())))] + //instruction_count:u32, + //#[br(count=instruction_count)] + //instructions:Vec> + + //please remember that strafe ticks are implicit! 33% smaller bot files +} + +pub struct StreamableBot{ + file:crate::file::File, + //timeline:timeline::Timeline, + segment_id_to_block_id:Vec, +} +impl StreamableBot{ + pub(crate) fn new(file:crate::file::File)->Result{ + Err(Error::InvalidHeader) + } + pub fn load_segment(&mut self,segment_id:SegmentId)->Result{ + let block_id=*self.segment_id_to_block_id.get(segment_id.get() as usize).ok_or(Error::InvalidSegmentId(segment_id))?; + let mut block=self.file.block_reader(block_id).map_err(Error::File)?; + let segment=block.read_le().map_err(Error::InvalidSegment)?; + Ok(segment) + } +} diff --git a/lib/snf/src/demo.rs b/lib/snf/src/demo.rs new file mode 100644 index 0000000..119290a --- /dev/null +++ b/lib/snf/src/demo.rs @@ -0,0 +1,31 @@ +use binrw::BinReaderExt; + +#[derive(Debug)] +pub enum Error{ + InvalidHeader, +} + +/* +BLOCK_DEMO_HEADER: +u128 map_resource_id +u64 map_header_block_id +u32 num_bots +for bot_id in 0..num_bots{ + u128 bot_resource_id + u64 bot_header_block_id +} + +//bot loading timeline +how to do worldstate for deathrun!? + +*/ + +pub struct StreamableDemo{ + map:Box>, + bots:Vec>, +} +impl StreamableDemo{ + pub(crate) fn new(file:crate::file::File)->Result{ + Err(Error::InvalidHeader) + } +} diff --git a/lib/snf/src/file.rs b/lib/snf/src/file.rs new file mode 100644 index 0000000..fb04c3e --- /dev/null +++ b/lib/snf/src/file.rs @@ -0,0 +1,110 @@ +//file format "sniff" + +use binrw::{binrw,BinReaderExt,io::TakeSeekExt}; + +#[derive(Debug)] +pub enum Error{ + InvalidHeader(binrw::Error), + UnexpectedEOF, + InvalidBlockId(BlockId), + Seek(std::io::Error), +} +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{} + +/* spec + +//begin global header + +//global metadata (32 bytes) +b"SNFB" +u32 format_version +u64 priming_bytes + //how many bytes of the file must be read to guarantee all of the expected + //format-specific metadata is available to facilitate streaming the remaining contents + //used by the database to guarantee that it serves at least the bare minimum +u128 resource_uuid + //identifies the file from anywhere for any other file + +//global block layout (variable size) +u64 num_blocks +//the start of the first block is implicitly after the global header (32) +//num_blocks+1 used in Header.block_location is implicitly the end of the file +for block_id in 1..num_blocks{ + u64 first_byte +} + +//end global header + +//begin blocks + +//each block is compressed with zstd or gz or something + +*/ +#[binrw] +#[brw(little)] +#[derive(Clone,Copy,Debug)] +pub(crate) enum FourCC{ + #[brw(magic=b"SNFM")] + Map, + #[brw(magic=b"SNFB")] + Bot, + #[brw(magic=b"SNFD")] + Demo, +} +#[binrw] +#[brw(little)] +#[derive(Debug)] +pub struct Header{ + /// Type of file + pub fourcc:FourCC, + /// File format version + pub version:u32, + /// Minimum data required to know the location of all streamable resources for this specific file + pub priming:u64, + /// uuid for this file + pub resource:u128, + //don't need try_calc: the struct will force field initialization anyways and it can be calculated there + pub block_count:u32, + #[br(count=block_count+1)] + pub block_location:Vec, +} + +#[binrw] +#[brw(little)] +#[derive(Clone,Copy,Debug,Hash,id::Id,Eq,Ord,PartialEq,PartialOrd)] +pub struct BlockId(u32); + +pub(crate) struct File{ + header:Header, + //reference to the data + data:R, +} + +impl File{ + pub(crate) fn new(mut input:R)->Result,Error>{ + Ok(File{ + header:input.read_le().map_err(Error::InvalidHeader)?, + data:input, + }) + } + pub(crate) fn data_mut(&mut self)->&mut R{ + &mut self.data + } + pub(crate) fn block_reader(&mut self,block_id:BlockId)->Result,Error>{ + if self.header.block_location.len() as u32<=block_id.get(){ + return Err(Error::InvalidBlockId(block_id)) + } + let block_start=self.header.block_location[block_id.get() as usize]; + let block_end=self.header.block_location[block_id.get() as usize+1]; + self.data.seek(std::io::SeekFrom::Start(block_start)).map_err(Error::Seek)?; + Ok(self.data_mut().take_seek(block_end-block_start)) + } + pub(crate) fn fourcc(&self)->FourCC{ + self.header.fourcc + } +} diff --git a/lib/snf/src/lib.rs b/lib/snf/src/lib.rs new file mode 100644 index 0000000..675dfdb --- /dev/null +++ b/lib/snf/src/lib.rs @@ -0,0 +1,68 @@ +use binrw::BinReaderExt; + +mod newtypes; + +mod file; +pub mod map; +pub mod bot; +pub mod demo; + +#[derive(Debug)] +pub enum Error{ + UnexpectedFourCC, + Header(file::Error), + Map(map::Error), + Bot(bot::Error), + Demo(demo::Error), +} +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{} + +pub enum SNF{ + Map(map::StreamableMap), + Bot(bot::StreamableBot), + Demo(demo::StreamableDemo), +} + +pub fn read_snf(input:R)->Result,Error>{ + let file=file::File::new(input).map_err(Error::Header)?; + Ok(match file.fourcc(){ + file::FourCC::Map=>SNF::Map(map::StreamableMap::new(file).map_err(Error::Map)?), + file::FourCC::Bot=>SNF::Bot(bot::StreamableBot::new(file).map_err(Error::Bot)?), + file::FourCC::Demo=>SNF::Demo(demo::StreamableDemo::new(file).map_err(Error::Demo)?), + }) +} +pub fn read_map(input:R)->Result,Error>{ + let file=file::File::new(input).map_err(Error::Header)?; + match file.fourcc(){ + file::FourCC::Map=>Ok(map::StreamableMap::new(file).map_err(Error::Map)?), + _=>Err(Error::UnexpectedFourCC) + } +} +pub fn read_bot(input:R)->Result,Error>{ + let file=file::File::new(input).map_err(Error::Header)?; + match file.fourcc(){ + file::FourCC::Bot=>Ok(bot::StreamableBot::new(file).map_err(Error::Bot)?), + _=>Err(Error::UnexpectedFourCC) + } +} +pub fn read_demo(input:R)->Result,Error>{ + let file=file::File::new(input).map_err(Error::Header)?; + match file.fourcc(){ + file::FourCC::Demo=>Ok(demo::StreamableDemo::new(file).map_err(Error::Demo)?), + _=>Err(Error::UnexpectedFourCC) + } +} + +#[cfg(test)] +mod tests { + //use super::*; + + #[test] + fn test() { + } +} diff --git a/lib/snf/src/map.rs b/lib/snf/src/map.rs new file mode 100644 index 0000000..a5ef6f7 --- /dev/null +++ b/lib/snf/src/map.rs @@ -0,0 +1,444 @@ +use std::io::Read; +use std::collections::HashMap; + +use crate::newtypes; +use crate::file::BlockId; +use binrw::{binrw,BinReaderExt,BinWriterExt}; +use strafesnet_common::model; +use strafesnet_common::aabb::Aabb; +use strafesnet_common::bvh::BvhNode; +use strafesnet_common::gameplay_modes; + +#[derive(Debug)] +pub enum Error{ + InvalidHeader(binrw::Error), + InvalidMode(newtypes::gameplay_modes::ModeError), + InvalidBlockId(BlockId), + InvalidMeshId(model::MeshId), + InvalidModelId(model::ModelId), + InvalidTextureId(model::TextureId), + InvalidData(binrw::Error), + IO(std::io::Error), + File(crate::file::Error), +} +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{} + +/* block types + +BLOCK_MAP_HEADER: +DefaultStyleInfo style_info +//bvh goes here +u32 num_nodes +u32 num_spacial_blocks +u32 num_resource_blocks +u32 num_resources_external +//node 0 parent node is implied to be None +for node_id in 1..num_nodes{ + u32 parent_node +} + +//NOTE: alternate realities are not necessary. +//portals/wormholes simply use an in-model and and out-model. +//skyboxes are inverted models with a special shader. + +//ideally spacial blocks are sorted from distance to start zone +//texture blocks are inserted before the first spacial block they are used in +for spacial_block_id in 0..num_spacial_blocks{ + u32 node_id + u32 block_id //data block + Aabb extents +} +//the order of these lists uniquely generates the incremental Ids +//MeshId, TextureId etc. based on resource type +//the first 8 bits of resource_uuid describe the type (mesh, texture, etc) +//if the map file references only external resources, num_resource_blocks = 0 +for resource_idx in 0..num_resource_blocks{ + Resource resource_type + u32 block_id +} +for resource_idx in 0..num_resources_external{ + u128 resource_uuid +} + +BLOCK_MAP_RESOURCE: +Resource resource_type +//an individual one of the following: + - model (Mesh) + - shader (compiled SPIR-V) + - image (JpegXL) + - sound (Opus) + - video (AV1) + - animation (Trey thing) + +BLOCK_MAP_REGION: +u64 num_models +for model_id in 0..num_models{ + ModelInstance mode_instance +} + +*/ + +//if you hash the resource itself and set the first 8 bits to this, that's the resource uuid +#[binrw] +#[brw(little,repr=u8)] +enum ResourceType{ + Mesh, + Texture, + //Shader, + //Sound, + //Video, + //Animation, +} +const RESOURCE_TYPE_VARIANT_COUNT:u8=2; +#[binrw] +#[brw(little)] +struct ResourceId(u128); +impl ResourceId{ + fn resource_type(&self)->Option{ + let discriminant=self.0 as u8; + //TODO: use this when it is stabilized https://github.com/rust-lang/rust/issues/73662 + //if (discriminant as usize)(){ + match discriminantSome(unsafe{std::mem::transmute::(discriminant)}), + false=>None, + } + } +} + +struct ResourceMap{ + meshes:HashMap, + textures:HashMap, +} +impl Default for ResourceMap{ + fn default()->Self{ + Self{ + meshes:HashMap::new(), + textures:HashMap::new(), + } + } +} + +#[binrw] +#[brw(little)] +struct SpacialBlockHeader{ + id:BlockId, + extents:newtypes::aabb::Aabb, +} +#[binrw] +#[brw(little)] +struct ResourceBlockHeader{ + resource:ResourceType, + id:BlockId, +} +#[binrw] +#[brw(little)] +struct ResourceExternalHeader{ + resource_uuid:ResourceId, +} + +#[binrw] +#[brw(little)] +struct MapHeader{ + num_spacial_blocks:u32, + num_resource_blocks:u32, + //num_resources_external:u32, + num_modes:u32, + num_attributes:u32, + num_render_configs:u32, + #[br(count=num_spacial_blocks)] + spacial_blocks:Vec, + #[br(count=num_resource_blocks)] + resource_blocks:Vec, + //#[br(count=num_resources_external)] + //external_resources:Vec, + #[br(count=num_modes)] + modes:Vec, + #[br(count=num_attributes)] + attributes:Vec, + #[br(count=num_render_configs)] + render_configs:Vec, +} + +#[binrw] +#[brw(little)] +struct Region{ + //consider including a bvh in the region instead of needing to rebalance the physics bvh on the fly + model_count:u32, + #[br(count=model_count)] + models:Vec<(u32,newtypes::model::Model)>, +} + +//code deduplication reused in into_complete_map +fn read_region(file:&mut crate::file::File,block_id:BlockId)->Result,Error>{ + //load region from disk + //parse the models and determine what resources need to be loaded + //load resources into self.resources + //return Region + let mut block=file.block_reader(block_id).map_err(Error::File)?; + let region:Region=block.read_le().map_err(Error::InvalidData)?; + Ok(region.models.into_iter().map(|(model_id,model)| + (model::ModelId::new(model_id),model.into()) + ).collect()) +} +fn read_mesh(file:&mut crate::file::File,block_id:BlockId)->Result{ + let mut block=file.block_reader(block_id).map_err(Error::File)?; + let mesh:newtypes::model::Mesh=block.read_le().map_err(Error::InvalidData)?; + Ok(mesh.into()) +} +fn read_texture(file:&mut crate::file::File,block_id:BlockId)->Result,Error>{ + let mut block=file.block_reader(block_id).map_err(Error::File)?; + let mut texture=Vec::new(); + block.read_to_end(&mut texture).map_err(Error::IO)?; + Ok(texture) +} + +pub struct StreamableMap{ + file:crate::file::File, + //this includes every platform... move the unconstrained datas to their appropriate data block? + modes:gameplay_modes::Modes, + //this is every possible attribute... need some sort of streaming system + attributes:Vec, + //this is every possible render configuration... shaders and such... need streaming + render_configs:Vec, + //this makes sense to keep in memory for streaming, a map of which blocks occupy what space + bvh:BvhNode, + //something something resources hashmaps + resource_blocks:ResourceMap, + //resource_external:ResourceMap, +} +impl StreamableMap{ + pub(crate) fn new(mut file:crate::file::File)->Result{ + //assume the file seek is in the right place to start reading a map header + let header:MapHeader=file.data_mut().read_le().map_err(Error::InvalidHeader)?; + let modes=header.modes.into_iter().map(TryInto::try_into).collect::>().map_err(Error::InvalidMode)?; + let attributes=header.attributes.into_iter().map(Into::into).collect(); + let render_configs=header.render_configs.into_iter().map(Into::into).collect(); + let bvh=header.spacial_blocks.into_iter().map(|spacial_block| + (spacial_block.id,spacial_block.extents.into()) + ).collect(); + //generate mesh ids and texture ids from resource list order + let mut resource_blocks=ResourceMap::default(); + for resource_block_header in header.resource_blocks{ + match resource_block_header.resource{ + ResourceType::Mesh=>{ + resource_blocks.meshes.insert( + //generate the id from the current length + model::MeshId::new(resource_blocks.meshes.len() as u32), + resource_block_header.id + ); + }, + ResourceType::Texture=>{ + resource_blocks.textures.insert( + model::TextureId::new(resource_blocks.textures.len() as u32), + resource_block_header.id + ); + }, + } + } + Ok(Self{ + file, + modes:strafesnet_common::gameplay_modes::Modes::new(modes), + attributes, + render_configs, + bvh:strafesnet_common::bvh::generate_bvh(bvh), + resource_blocks, + //resource_external:Default::default(), + }) + } + pub fn get_intersecting_region_block_ids(&self,aabb:&Aabb)->Vec{ + let mut block_ids=Vec::new(); + self.bvh.the_tester(aabb,&mut |&block_id|block_ids.push(block_id)); + block_ids + } + pub fn load_region(&mut self,block_id:BlockId)->Result,Error>{ + read_region(&mut self.file,block_id) + } + pub fn load_mesh(&mut self,mesh_id:model::MeshId)->Result{ + let block_id=*self.resource_blocks.meshes.get(&mesh_id).ok_or(Error::InvalidMeshId(mesh_id))?; + read_mesh(&mut self.file,block_id) + } + pub fn load_texture(&mut self,texture_id:model::TextureId)->Result,Error>{ + let block_id=*self.resource_blocks.textures.get(&texture_id).ok_or(Error::InvalidTextureId(texture_id))?; + read_texture(&mut self.file,block_id) + } + pub fn into_complete_map(mut self)->Result{ + let mut block_ids=Vec::new(); + self.bvh.into_visitor(&mut |block_id|block_ids.push(block_id)); + //count on reading the file in sequential order being fastest + block_ids.sort_unstable(); + //load all regions + let mut model_pairs=HashMap::new(); + for block_id in block_ids{ + model_pairs.extend(read_region(&mut self.file,block_id)?); + } + let mut models=Vec::with_capacity(model_pairs.len()); + for model_id in 0..model_pairs.len() as u32{ + let model_id=model::ModelId::new(model_id); + models.push(model_pairs.remove(&model_id).ok_or(Error::InvalidModelId(model_id))?); + } + //load all meshes + let mut meshes=Vec::with_capacity(self.resource_blocks.meshes.len()); + for mesh_id in 0..self.resource_blocks.meshes.len() as u32{ + let mesh_id=model::MeshId::new(mesh_id); + let block_id=self.resource_blocks.meshes[&mesh_id]; + meshes.push(read_mesh(&mut self.file,block_id)?); + }; + //load all textures + let mut textures=Vec::with_capacity(self.resource_blocks.textures.len()); + for texture_id in 0..self.resource_blocks.textures.len() as u32{ + let texture_id=model::TextureId::new(texture_id); + let block_id=self.resource_blocks.textures[&texture_id]; + textures.push(read_texture(&mut self.file,block_id)?); + } + Ok(strafesnet_common::map::CompleteMap{ + modes:self.modes, + attributes:self.attributes, + meshes, + models, + textures, + render_configs:self.render_configs, + }) + } +} + +const BVH_NODE_MAX_WEIGHT:usize=64*1024;//64 kB +fn collect_spacial_blocks( + block_location:&mut Vec, + block_headers:&mut Vec, + sequential_block_data:&mut std::io::Cursor<&mut Vec>, + bvh_node:strafesnet_common::bvh::BvhWeightNode +)->Result<(),Error>{ + //inspect the node weights top-down. + //When a node weighs less than the limit, + //serialize its entire contents into a region block + if *bvh_node.weight(){ + for bvh_node in bvh_node_list{ + collect_spacial_blocks(block_location,block_headers,sequential_block_data,bvh_node)?; + } + }, + strafesnet_common::bvh::RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum + } + } + Ok(()) +} + +/// TODO: Optionally provide a bot that describes the path through the map +/// otherwise sort by distance to start zone +pub fn write_map(mut writer:W,map:strafesnet_common::map::CompleteMap)->Result<(),Error>{ + //serialize models and make a bvh that knows the file size of the branch + let boxen=map.models.into_iter().enumerate().map(|(model_id,model)|{ + //grow your own aabb + let mesh=map.meshes.get(model.mesh.get() as usize).ok_or(Error::InvalidMeshId(model.mesh))?; + let mut aabb=strafesnet_common::aabb::Aabb::default(); + for &pos in &mesh.unique_pos{ + aabb.grow(model.transform.transform_point3(pos).fix_1()); + } + Ok(((model::ModelId::new(model_id as u32),model.into()),aabb)) + }).collect::,_>>()?; + let bvh=strafesnet_common::bvh::generate_bvh(boxen).weigh_contents(&|_|std::mem::size_of::()); + //build blocks + //block location is initialized with two values + //the first value represents the location of the first byte after the file header + //the second value represents the first byte of the second data block + //the first data block is always the map header, so the difference is the map header size. + //this information is filled in later after the sizes are known. + let mut block_location=vec![0,0];//for file header + let mut spacial_blocks=Vec::new();//for map header + let mut sequential_block_data=Vec::new(); + let mut cursor_to_data=std::io::Cursor::new(&mut sequential_block_data); + collect_spacial_blocks(&mut block_location,&mut spacial_blocks,&mut cursor_to_data,bvh)?; + let mut block_count=spacial_blocks.len() as u32+1;//continue block id + let mut resource_blocks=Vec::new();//for map header + //meshes + for mesh in map.meshes.into_iter(){ + resource_blocks.push(ResourceBlockHeader{ + resource:ResourceType::Mesh, + id:BlockId::new(block_count), + }); + block_count+=1; + let serializable_mesh:newtypes::model::Mesh=mesh.into(); + binrw::BinWrite::write_le(&serializable_mesh,&mut cursor_to_data).map_err(Error::InvalidData)?; + block_location.push(cursor_to_data.position()); + } + //textures + for mut texture in map.textures.into_iter(){ + resource_blocks.push(ResourceBlockHeader{ + resource:ResourceType::Texture, + id:BlockId::new(block_count), + }); + block_count+=1; + sequential_block_data.append(&mut texture); + block_location.push(sequential_block_data.len() as u64); + } + //build header + let map_header=MapHeader{ + num_spacial_blocks:spacial_blocks.len() as u32, + num_resource_blocks:resource_blocks.len() as u32, + //num_resources_external:0, + num_modes:map.modes.modes.len() as u32, + num_attributes:map.attributes.len() as u32, + num_render_configs:map.render_configs.len() as u32, + spacial_blocks, + resource_blocks, + //external_resources:Vec::new(), + modes:map.modes.modes.into_iter().map(Into::into).collect(), + attributes:map.attributes.into_iter().map(Into::into).collect(), + render_configs:map.render_configs.into_iter().map(Into::into).collect(), + }; + let mut file_header=crate::file::Header{ + fourcc:crate::file::FourCC::Map, + version:0, + priming:0, + resource:0, + block_count, + block_location, + }; + //probe header length + let mut file_header_data=Vec::new(); + binrw::BinWrite::write_le(&file_header,&mut std::io::Cursor::new(&mut file_header_data)).map_err(Error::InvalidData)?; + let mut map_header_data=Vec::new(); + binrw::BinWrite::write_le(&map_header,&mut std::io::Cursor::new(&mut map_header_data)).map_err(Error::InvalidData)?; + + //update file header according to probe data + let mut offset=file_header_data.len() as u64; + file_header.priming=offset; + file_header.block_location[0]=offset; + offset+=map_header_data.len() as u64; + for position in &mut file_header.block_location[1..]{ + *position+=offset; + } + + //write (updated) file header + writer.write_le(&file_header).map_err(Error::InvalidData)?; + //write map header + writer.write(&map_header_data).map_err(Error::IO)?; + //write blocks + writer.write(&sequential_block_data).map_err(Error::IO)?; + Ok(()) +} diff --git a/lib/snf/src/newtypes.rs b/lib/snf/src/newtypes.rs new file mode 100644 index 0000000..19e2e59 --- /dev/null +++ b/lib/snf/src/newtypes.rs @@ -0,0 +1,7 @@ +mod common; +pub mod aabb; +pub mod model; +pub mod integer; +pub mod gameplay_modes; +pub mod gameplay_style; +pub mod gameplay_attributes; diff --git a/lib/snf/src/newtypes/aabb.rs b/lib/snf/src/newtypes/aabb.rs new file mode 100644 index 0000000..4443e55 --- /dev/null +++ b/lib/snf/src/newtypes/aabb.rs @@ -0,0 +1,23 @@ +use super::integer::Planar64Vec3; +#[binrw::binrw] +#[brw(little)] +pub struct Aabb{ + pub min:Planar64Vec3, + pub max:Planar64Vec3, +} +impl Into for Aabb{ + fn into(self)->strafesnet_common::aabb::Aabb{ + strafesnet_common::aabb::Aabb::new( + strafesnet_common::integer::vec3::raw_array(self.min), + strafesnet_common::integer::vec3::raw_array(self.max), + ) + } +} +impl From for Aabb{ + fn from(value:strafesnet_common::aabb::Aabb)->Self{ + Self{ + max:value.max().map(|t|t.to_raw()).to_array(), + min:value.min().map(|t|t.to_raw()).to_array(), + } + } +} diff --git a/lib/snf/src/newtypes/common.rs b/lib/snf/src/newtypes/common.rs new file mode 100644 index 0000000..cec7e09 --- /dev/null +++ b/lib/snf/src/newtypes/common.rs @@ -0,0 +1,3 @@ +pub const fn flag(b:bool,mask:u8)->u8{ + (-(b as i8) as u8)&mask +} diff --git a/lib/snf/src/newtypes/gameplay_attributes.rs b/lib/snf/src/newtypes/gameplay_attributes.rs new file mode 100644 index 0000000..3dcfb81 --- /dev/null +++ b/lib/snf/src/newtypes/gameplay_attributes.rs @@ -0,0 +1,493 @@ +use super::common::flag; +use super::integer::{Time,Planar64,Planar64Vec3}; + +#[binrw::binrw] +#[brw(little)] +pub struct ContactingLadder{ + #[br(map=|paused:u8|paused!=0)] + #[bw(map=|paused:&bool|*paused as u8)] + pub sticky:bool, +} +impl Into for ContactingLadder{ + fn into(self)->strafesnet_common::gameplay_attributes::ContactingLadder{ + strafesnet_common::gameplay_attributes::ContactingLadder{ + sticky:self.sticky.into(), + } + } +} +impl From for ContactingLadder{ + fn from(value:strafesnet_common::gameplay_attributes::ContactingLadder)->Self{ + Self{ + sticky:value.sticky.into(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub enum ContactingBehaviour{ + #[brw(magic=0u8)] + Surf, + #[brw(magic=1u8)] + Ladder(ContactingLadder), + #[brw(magic=2u8)] + NoJump, + #[brw(magic=3u8)] + Cling, + #[brw(magic=4u8)] + Elastic(u32), +} +impl Into for ContactingBehaviour{ + fn into(self)->strafesnet_common::gameplay_attributes::ContactingBehaviour{ + match self{ + ContactingBehaviour::Surf=> + strafesnet_common::gameplay_attributes::ContactingBehaviour::Surf, + ContactingBehaviour::Ladder(contacting_ladder)=> + strafesnet_common::gameplay_attributes::ContactingBehaviour::Ladder( + contacting_ladder.into(), + ), + ContactingBehaviour::NoJump=> + strafesnet_common::gameplay_attributes::ContactingBehaviour::NoJump, + ContactingBehaviour::Cling=> + strafesnet_common::gameplay_attributes::ContactingBehaviour::Cling, + ContactingBehaviour::Elastic(elasticity)=> + strafesnet_common::gameplay_attributes::ContactingBehaviour::Elastic(elasticity), + } + } +} +impl From for ContactingBehaviour{ + fn from(value:strafesnet_common::gameplay_attributes::ContactingBehaviour)->Self{ + match value{ + strafesnet_common::gameplay_attributes::ContactingBehaviour::Surf=> + ContactingBehaviour::Surf, + strafesnet_common::gameplay_attributes::ContactingBehaviour::Ladder(contacting_ladder)=> + ContactingBehaviour::Ladder( + contacting_ladder.into() + ), + strafesnet_common::gameplay_attributes::ContactingBehaviour::NoJump=> + ContactingBehaviour::NoJump, + strafesnet_common::gameplay_attributes::ContactingBehaviour::Cling=> + ContactingBehaviour::Cling, + strafesnet_common::gameplay_attributes::ContactingBehaviour::Elastic(elasticity)=> + ContactingBehaviour::Elastic(elasticity), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct IntersectingWater{ + pub viscosity:Planar64, + pub density:Planar64, + pub velocity:Planar64Vec3, +} +impl Into for IntersectingWater{ + fn into(self)->strafesnet_common::gameplay_attributes::IntersectingWater{ + strafesnet_common::gameplay_attributes::IntersectingWater{ + viscosity:strafesnet_common::integer::Planar64::raw(self.viscosity), + density:strafesnet_common::integer::Planar64::raw(self.density), + velocity:strafesnet_common::integer::vec3::raw_array(self.velocity), + } + } +} +impl From for IntersectingWater{ + fn from(value:strafesnet_common::gameplay_attributes::IntersectingWater)->Self{ + Self{ + viscosity:value.viscosity.to_raw(), + density:value.density.to_raw(), + velocity:value.velocity.map(|t|t.to_raw()).to_array(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Accelerator{ + pub acceleration:Planar64Vec3 +} +impl Into for Accelerator{ + fn into(self)->strafesnet_common::gameplay_attributes::Accelerator{ + strafesnet_common::gameplay_attributes::Accelerator{ + acceleration:strafesnet_common::integer::vec3::raw_array(self.acceleration) + } + } +} +impl From for Accelerator{ + fn from(value:strafesnet_common::gameplay_attributes::Accelerator)->Self{ + Self{ + acceleration:value.acceleration.map(|t|t.to_raw()).to_array(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub enum Booster{ + #[brw(magic=0u8)] + Velocity(Planar64Vec3), + #[brw(magic=1u8)] + Energy{direction:Planar64Vec3,energy:Planar64}, + #[brw(magic=2u8)] + AirTime(Time), + #[brw(magic=3u8)] + Height(Planar64), + +} +impl Into for Booster{ + fn into(self)->strafesnet_common::gameplay_attributes::Booster{ + match self{ + Booster::Velocity(velocity)=> + strafesnet_common::gameplay_attributes::Booster::Velocity( + strafesnet_common::integer::vec3::raw_array(velocity) + ), + Booster::Energy{direction,energy}=> + strafesnet_common::gameplay_attributes::Booster::Energy{ + direction:strafesnet_common::integer::vec3::raw_array(direction), + energy:strafesnet_common::integer::Planar64::raw(energy) + }, + Booster::AirTime(time)=> + strafesnet_common::gameplay_attributes::Booster::AirTime( + strafesnet_common::integer::Time::raw(time) + ), + Booster::Height(height)=> + strafesnet_common::gameplay_attributes::Booster::Height( + strafesnet_common::integer::Planar64::raw(height) + ), + } + } +} +impl From for Booster{ + fn from(value:strafesnet_common::gameplay_attributes::Booster)->Self{ + match value{ + strafesnet_common::gameplay_attributes::Booster::Velocity(velocity)=> + Booster::Velocity(velocity.map(|t|t.to_raw()).to_array()), + strafesnet_common::gameplay_attributes::Booster::Energy{direction,energy}=> + Booster::Energy{ + direction:direction.map(|t|t.to_raw()).to_array(), + energy:energy.to_raw(), + }, + strafesnet_common::gameplay_attributes::Booster::AirTime(time)=> + Booster::AirTime(time.get()), + strafesnet_common::gameplay_attributes::Booster::Height(height)=> + Booster::Height(height.to_raw()), + } + } +} + +#[binrw::binrw] +#[brw(little,repr=u8)] +pub enum TrajectoryChoice{ + HighArcLongDuration, + LowArcShortDuration, +} +impl Into for TrajectoryChoice{ + fn into(self)->strafesnet_common::gameplay_attributes::TrajectoryChoice{ + match self{ + TrajectoryChoice::HighArcLongDuration=> + strafesnet_common::gameplay_attributes::TrajectoryChoice::HighArcLongDuration, + TrajectoryChoice::LowArcShortDuration=> + strafesnet_common::gameplay_attributes::TrajectoryChoice::LowArcShortDuration, + } + } +} +impl From for TrajectoryChoice{ + fn from(value:strafesnet_common::gameplay_attributes::TrajectoryChoice)->Self{ + match value{ + strafesnet_common::gameplay_attributes::TrajectoryChoice::HighArcLongDuration=> + TrajectoryChoice::HighArcLongDuration, + strafesnet_common::gameplay_attributes::TrajectoryChoice::LowArcShortDuration=> + TrajectoryChoice::LowArcShortDuration, + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub enum SetTrajectory{ + #[brw(magic=0u8)] + AirTime(Time), + #[brw(magic=1u8)] + Height(Planar64), + #[brw(magic=2u8)] + DotVelocity{direction:Planar64Vec3,dot:Planar64}, + #[brw(magic=3u8)] + TargetPointTime{ + target_point:Planar64Vec3, + time:Time, + }, + #[brw(magic=4u8)] + TargetPointSpeed{ + target_point:Planar64Vec3, + speed:Planar64, + trajectory_choice:TrajectoryChoice, + }, + #[brw(magic=5u8)] + Velocity(Planar64Vec3), +} +impl Into for SetTrajectory{ + fn into(self)->strafesnet_common::gameplay_attributes::SetTrajectory{ + match self{ + SetTrajectory::AirTime(time)=> + strafesnet_common::gameplay_attributes::SetTrajectory::AirTime( + strafesnet_common::integer::Time::raw(time) + ), + SetTrajectory::Height(height)=> + strafesnet_common::gameplay_attributes::SetTrajectory::Height( + strafesnet_common::integer::Planar64::raw(height) + ), + SetTrajectory::DotVelocity{direction,dot}=> + strafesnet_common::gameplay_attributes::SetTrajectory::DotVelocity{ + direction:strafesnet_common::integer::vec3::raw_array(direction), + dot:strafesnet_common::integer::Planar64::raw(dot), + }, + SetTrajectory::TargetPointTime{target_point,time}=> + strafesnet_common::gameplay_attributes::SetTrajectory::TargetPointTime{ + target_point:strafesnet_common::integer::vec3::raw_array(target_point), + time:strafesnet_common::integer::Time::raw(time), + }, + SetTrajectory::TargetPointSpeed{target_point,speed,trajectory_choice}=> + strafesnet_common::gameplay_attributes::SetTrajectory::TargetPointSpeed{ + target_point:strafesnet_common::integer::vec3::raw_array(target_point), + speed:strafesnet_common::integer::Planar64::raw(speed), + trajectory_choice:trajectory_choice.into(), + }, + SetTrajectory::Velocity(velocity)=> + strafesnet_common::gameplay_attributes::SetTrajectory::Velocity( + strafesnet_common::integer::vec3::raw_array(velocity) + ), + } + } +} +impl From for SetTrajectory{ + fn from(value:strafesnet_common::gameplay_attributes::SetTrajectory)->Self{ + match value{ + strafesnet_common::gameplay_attributes::SetTrajectory::AirTime(time)=> + SetTrajectory::AirTime( + time.get() + ), + strafesnet_common::gameplay_attributes::SetTrajectory::Height(height)=> + SetTrajectory::Height( + height.to_raw() + ), + strafesnet_common::gameplay_attributes::SetTrajectory::DotVelocity{direction,dot}=> + SetTrajectory::DotVelocity{ + direction:direction.map(|t|t.to_raw()).to_array(), + dot:dot.to_raw(), + }, + strafesnet_common::gameplay_attributes::SetTrajectory::TargetPointTime{target_point,time}=> + SetTrajectory::TargetPointTime{ + target_point:target_point.map(|t|t.to_raw()).to_array(), + time:time.get(), + }, + strafesnet_common::gameplay_attributes::SetTrajectory::TargetPointSpeed{target_point,speed,trajectory_choice}=> + SetTrajectory::TargetPointSpeed{ + target_point:target_point.map(|t|t.to_raw()).to_array(), + speed:speed.to_raw(), + trajectory_choice:trajectory_choice.into(), + }, + strafesnet_common::gameplay_attributes::SetTrajectory::Velocity(velocity)=> + SetTrajectory::Velocity( + velocity.map(|t|t.to_raw()).to_array() + ), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Wormhole{ + pub destination_model:u32, +} +impl Into for Wormhole{ + fn into(self)->strafesnet_common::gameplay_attributes::Wormhole{ + strafesnet_common::gameplay_attributes::Wormhole{ + destination_model:strafesnet_common::model::ModelId::new(self.destination_model), + } + } +} +impl From for Wormhole{ + fn from(value:strafesnet_common::gameplay_attributes::Wormhole)->Self{ + Self{ + destination_model:value.destination_model.get(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct GeneralAttributes{ + pub header:u8, + #[br(if(header&Self::BOOSTER!=0))] + pub booster:Option, + #[br(if(header&Self::TRAJECTORY!=0))] + pub trajectory:Option, + #[br(if(header&Self::WORMHOLE!=0))] + pub wormhole:Option, + #[br(if(header&Self::ACCELERATOR!=0))] + pub accelerator:Option, +} +impl GeneralAttributes{ + const BOOSTER:u8=1<<0; + const TRAJECTORY:u8=1<<1; + const WORMHOLE:u8=1<<2; + const ACCELERATOR:u8=1<<3; +} +impl Into for GeneralAttributes{ + fn into(self)->strafesnet_common::gameplay_attributes::GeneralAttributes{ + strafesnet_common::gameplay_attributes::GeneralAttributes{ + booster:self.booster.map(Into::into), + trajectory:self.trajectory.map(Into::into), + wormhole:self.wormhole.map(Into::into), + accelerator:self.accelerator.map(Into::into), + } + } +} +impl From for GeneralAttributes{ + fn from(value:strafesnet_common::gameplay_attributes::GeneralAttributes)->Self{ + let header= + flag(value.booster.is_some(),GeneralAttributes::BOOSTER) + |flag(value.trajectory.is_some(),GeneralAttributes::TRAJECTORY) + |flag(value.wormhole.is_some(),GeneralAttributes::WORMHOLE) + |flag(value.accelerator.is_some(),GeneralAttributes::ACCELERATOR); + Self{ + header, + booster:value.booster.map(Into::into), + trajectory:value.trajectory.map(Into::into), + wormhole:value.wormhole.map(Into::into), + accelerator:value.accelerator.map(Into::into), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct ContactingAttributes{ + pub header:u8, + #[br(if(header&Self::CONTACTING_BEHAVIOUR!=0))] + pub contact_behaviour:Option, +} +impl ContactingAttributes{ + const CONTACTING_BEHAVIOUR:u8=1<<0; +} +impl Into for ContactingAttributes{ + fn into(self)->strafesnet_common::gameplay_attributes::ContactingAttributes{ + strafesnet_common::gameplay_attributes::ContactingAttributes{ + contact_behaviour:self.contact_behaviour.map(Into::into), + } + } +} +impl From for ContactingAttributes{ + fn from(value:strafesnet_common::gameplay_attributes::ContactingAttributes)->Self{ + Self{ + header:flag(value.contact_behaviour.is_some(),ContactingAttributes::CONTACTING_BEHAVIOUR), + contact_behaviour:value.contact_behaviour.map(Into::into), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct IntersectingAttributes{ + pub header:u8, + #[br(if(header&Self::INTERSECTING_WATER!=0))] + pub water:Option, +} +impl IntersectingAttributes{ + const INTERSECTING_WATER:u8=1<<0; +} +impl Into for IntersectingAttributes{ + fn into(self)->strafesnet_common::gameplay_attributes::IntersectingAttributes{ + strafesnet_common::gameplay_attributes::IntersectingAttributes{ + water:self.water.map(Into::into), + } + } +} +impl From for IntersectingAttributes{ + fn from(value:strafesnet_common::gameplay_attributes::IntersectingAttributes)->Self{ + Self{ + header:flag(value.water.is_some(),IntersectingAttributes::INTERSECTING_WATER), + water:value.water.map(Into::into), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct ContactAttributes{ + contacting:ContactingAttributes, + general:GeneralAttributes, +} +impl Into for ContactAttributes{ + fn into(self)->strafesnet_common::gameplay_attributes::ContactAttributes{ + strafesnet_common::gameplay_attributes::ContactAttributes{ + contacting:self.contacting.into(), + general:self.general.into(), + } + } +} +impl From for ContactAttributes{ + fn from(value:strafesnet_common::gameplay_attributes::ContactAttributes)->Self{ + Self{ + contacting:value.contacting.into(), + general:value.general.into(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct IntersectAttributes{ + intersecting:IntersectingAttributes, + general:GeneralAttributes, +} +impl Into for IntersectAttributes{ + fn into(self)->strafesnet_common::gameplay_attributes::IntersectAttributes{ + strafesnet_common::gameplay_attributes::IntersectAttributes{ + intersecting:self.intersecting.into(), + general:self.general.into(), + } + } +} +impl From for IntersectAttributes{ + fn from(value:strafesnet_common::gameplay_attributes::IntersectAttributes)->Self{ + Self{ + intersecting:value.intersecting.into(), + general:value.general.into(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub enum CollisionAttributes{ + #[brw(magic=0u8)] + Decoration, + #[brw(magic=1u8)] + Contact(ContactAttributes), + #[brw(magic=2u8)] + Intersect(IntersectAttributes), +} +impl Into for CollisionAttributes{ + fn into(self)->strafesnet_common::gameplay_attributes::CollisionAttributes{ + match self{ + CollisionAttributes::Decoration=> + strafesnet_common::gameplay_attributes::CollisionAttributes::Decoration, + CollisionAttributes::Contact(attr)=> + strafesnet_common::gameplay_attributes::CollisionAttributes::Contact(attr.into()), + CollisionAttributes::Intersect(attr)=> + strafesnet_common::gameplay_attributes::CollisionAttributes::Intersect(attr.into()), + } + } +} +impl From for CollisionAttributes{ + fn from(value:strafesnet_common::gameplay_attributes::CollisionAttributes)->Self{ + match value{ + strafesnet_common::gameplay_attributes::CollisionAttributes::Decoration=> + CollisionAttributes::Decoration, + strafesnet_common::gameplay_attributes::CollisionAttributes::Contact(attr)=> + CollisionAttributes::Contact(attr.into()), + strafesnet_common::gameplay_attributes::CollisionAttributes::Intersect(attr)=> + CollisionAttributes::Intersect(attr.into()), + } + } +} diff --git a/lib/snf/src/newtypes/gameplay_modes.rs b/lib/snf/src/newtypes/gameplay_modes.rs new file mode 100644 index 0000000..4363208 --- /dev/null +++ b/lib/snf/src/newtypes/gameplay_modes.rs @@ -0,0 +1,220 @@ +use super::common::flag; + +pub type ModeId=u32; +pub type StageId=u32; + +#[binrw::binrw] +#[brw(little)] +pub struct StageElement{ + pub header:u8, + pub stage_id:StageId, + #[br(if(header&Self::JUMP_LIMIT!=0))] + pub jump_limit:Option, +} +impl StageElement{ + const BEHAVIOUR:u8=0b00111; + const JUMP_LIMIT:u8=1<<3; + const FORCE:u8=1<<4; + const fn behaviour(&self)->Option{ + match self.header&Self::BEHAVIOUR{ + 0=>Some(strafesnet_common::gameplay_modes::StageElementBehaviour::SpawnAt), + 1=>Some(strafesnet_common::gameplay_modes::StageElementBehaviour::Trigger), + 2=>Some(strafesnet_common::gameplay_modes::StageElementBehaviour::Teleport), + 3=>Some(strafesnet_common::gameplay_modes::StageElementBehaviour::Platform), + 4=>Some(strafesnet_common::gameplay_modes::StageElementBehaviour::Check), + 5=>Some(strafesnet_common::gameplay_modes::StageElementBehaviour::Checkpoint), + _=>None, + } + } + const fn force(&self)->bool{ + self.header&Self::FORCE!=0 + } +} +#[derive(Debug)] +pub enum StageElementError{ + InvalidBehaviour, +} +impl std::fmt::Display for StageElementError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for StageElementError{} +impl TryInto for StageElement{ + type Error=StageElementError; + fn try_into(self)->Result{ + Ok(strafesnet_common::gameplay_modes::StageElement::new( + strafesnet_common::gameplay_modes::StageId::new(self.stage_id), + self.force(), + self.behaviour().ok_or(StageElementError::InvalidBehaviour)?, + self.jump_limit, + )) + } +} +impl From for StageElement{ + fn from(value:strafesnet_common::gameplay_modes::StageElement)->Self{ + let behaviour=match value.behaviour(){ + strafesnet_common::gameplay_modes::StageElementBehaviour::SpawnAt=>0, + strafesnet_common::gameplay_modes::StageElementBehaviour::Trigger=>1, + strafesnet_common::gameplay_modes::StageElementBehaviour::Teleport=>2, + strafesnet_common::gameplay_modes::StageElementBehaviour::Platform=>3, + strafesnet_common::gameplay_modes::StageElementBehaviour::Check=>4, + strafesnet_common::gameplay_modes::StageElementBehaviour::Checkpoint=>5, + }; + let header= + behaviour + |flag(value.jump_limit().is_some(),StageElement::JUMP_LIMIT) + |flag(value.force(),StageElement::FORCE); + Self{ + header, + stage_id:value.stage_id().get(), + jump_limit:value.jump_limit(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Stage{ + pub spawn:u32, + //open world support lol + pub ordered_checkpoints_count:u32, + pub unordered_checkpoints_count:u32, + //currently loaded checkpoint models + #[br(count=ordered_checkpoints_count)] + pub ordered_checkpoints:Vec<(u32,u32)>, + #[br(count=unordered_checkpoints_count)] + pub unordered_checkpoints:Vec, +} +impl Into for Stage{ + fn into(self)->strafesnet_common::gameplay_modes::Stage{ + strafesnet_common::gameplay_modes::Stage::new( + strafesnet_common::model::ModelId::new(self.spawn), + self.ordered_checkpoints_count, + self.unordered_checkpoints_count, + self.ordered_checkpoints.into_iter().map(|(checkpoint_id,model_id)|( + strafesnet_common::gameplay_modes::CheckpointId::new(checkpoint_id), + strafesnet_common::model::ModelId::new(model_id), + )).collect(), + self.unordered_checkpoints.into_iter() + .map(strafesnet_common::model::ModelId::new) + .collect(), + ) + } +} +impl From for Stage{ + fn from(value:strafesnet_common::gameplay_modes::Stage)->Self{ + let spawn=value.spawn().get(); + let ordered_checkpoints_count=value.ordered_checkpoints_count(); + let unordered_checkpoints_count=value.unordered_checkpoints_count(); + let (ordered_checkpoints,unordered_checkpoints)=value.into_inner(); + Self{ + spawn, + ordered_checkpoints_count, + unordered_checkpoints_count, + ordered_checkpoints:ordered_checkpoints.into_iter() + .map(|(checkpoint_id,model_id)|(checkpoint_id.get(),model_id.get())) + .collect(), + unordered_checkpoints:unordered_checkpoints.into_iter() + .map(|model_id|model_id.get()) + .collect(), + } + } +} + +#[binrw::binrw] +#[brw(little,repr=u8)] +pub enum Zone{ + Start, + Finish, + Anticheat, +} +impl Into for Zone{ + fn into(self)->strafesnet_common::gameplay_modes::Zone{ + match self{ + Zone::Start=>strafesnet_common::gameplay_modes::Zone::Start, + Zone::Finish=>strafesnet_common::gameplay_modes::Zone::Finish, + Zone::Anticheat=>strafesnet_common::gameplay_modes::Zone::Anticheat, + } + } +} +impl From for Zone{ + fn from(value:strafesnet_common::gameplay_modes::Zone)->Self{ + match value{ + strafesnet_common::gameplay_modes::Zone::Start=>Zone::Start, + strafesnet_common::gameplay_modes::Zone::Finish=>Zone::Finish, + strafesnet_common::gameplay_modes::Zone::Anticheat=>Zone::Anticheat, + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct ModeHeader{ + pub zones:u32, + pub stages:u32, + pub elements:u32, +} +#[binrw::binrw] +#[brw(little)] +pub struct Mode{ + pub header:ModeHeader, + pub style:super::gameplay_style::StyleModifiers, + pub start:u32, + #[br(count=header.zones)] + pub zones:Vec<(u32,Zone)>, + #[br(count=header.stages)] + pub stages:Vec, + #[br(count=header.elements)] + pub elements:Vec<(u32,StageElement)>, +} +#[derive(Debug)] +pub enum ModeError{ + StyleModifier(super::gameplay_style::StyleModifierError), + StageElement(StageElementError), +} +impl std::fmt::Display for ModeError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for ModeError{} +impl TryInto for Mode{ + type Error=ModeError; + fn try_into(self)->Result{ + Ok(strafesnet_common::gameplay_modes::Mode::new( + self.style.try_into().map_err(ModeError::StyleModifier)?, + strafesnet_common::model::ModelId::new(self.start), + self.zones.into_iter().map(|(model_id,zone)| + (strafesnet_common::model::ModelId::new(model_id),zone.into()) + ).collect(), + self.stages.into_iter().map(Into::into).collect(), + self.elements.into_iter().map(|(model_id,stage_element)| + Ok((strafesnet_common::model::ModelId::new(model_id),stage_element.try_into()?)) + ).collect::>().map_err(ModeError::StageElement)?, + )) + } +} +impl From for Mode{ + fn from(value:strafesnet_common::gameplay_modes::Mode)->Self{ + let (style,start,zones,stages,elements)=value.into_inner(); + Self{ + header:ModeHeader{ + zones:zones.len() as u32, + stages:stages.len() as u32, + elements:elements.len() as u32, + }, + style:style.into(), + start:start.get(), + zones:zones.into_iter() + .map(|(model_id,zone)|(model_id.get(),zone.into())) + .collect(), + stages:stages.into_iter() + .map(Into::into) + .collect(), + elements:elements.into_iter() + .map(|(model_id,stage_element)|(model_id.get(),stage_element.into())) + .collect(), + } + } +} diff --git a/lib/snf/src/newtypes/gameplay_style.rs b/lib/snf/src/newtypes/gameplay_style.rs new file mode 100644 index 0000000..9033700 --- /dev/null +++ b/lib/snf/src/newtypes/gameplay_style.rs @@ -0,0 +1,442 @@ +use super::common::flag; +use super::integer::{Time,Ratio64,Planar64,Planar64Vec3}; + +pub type Controls=u32; +#[derive(Debug)] +pub enum ControlsError{ + UnknownBits, +} +impl std::fmt::Display for ControlsError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for ControlsError{} + +#[binrw::binrw] +#[brw(little)] +pub struct StyleModifiers{ + pub header:u8, + pub controls_mask:Controls, + pub controls_mask_state:Controls, + #[br(if(header&Self::STRAFE!=0))] + pub strafe:Option, + #[br(if(header&Self::ROCKET!=0))] + pub rocket:Option, + #[br(if(header&Self::JUMP!=0))] + pub jump:Option, + #[br(if(header&Self::WALK!=0))] + pub walk:Option, + #[br(if(header&Self::LADDER!=0))] + pub ladder:Option, + #[br(if(header&Self::SWIM!=0))] + pub swim:Option, + pub gravity:Planar64Vec3, + pub hitbox:Hitbox, + pub camera_offset:Planar64Vec3, + pub mass:Planar64, +} +impl StyleModifiers{ + const STRAFE:u8=1<<0; + const ROCKET:u8=1<<1; + const JUMP:u8=1<<2; + const WALK:u8=1<<3; + const LADDER:u8=1<<4; + const SWIM:u8=1<<5; +} +#[derive(Debug)] +pub enum StyleModifierError{ + Controls(ControlsError), + JumpSettings(JumpSettingsError), + StrafeSettings(StrafeSettingsError), +} +impl std::fmt::Display for StyleModifierError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for StyleModifierError{} +impl TryInto for StyleModifiers{ + type Error=StyleModifierError; + fn try_into(self)->Result{ + Ok(strafesnet_common::gameplay_style::StyleModifiers{ + controls_mask:strafesnet_common::controls_bitflag::Controls::from_bits(self.controls_mask).ok_or(StyleModifierError::Controls(ControlsError::UnknownBits))?, + controls_mask_state:strafesnet_common::controls_bitflag::Controls::from_bits(self.controls_mask_state).ok_or(StyleModifierError::Controls(ControlsError::UnknownBits))?, + strafe:self.strafe.map(TryInto::try_into).transpose().map_err(StyleModifierError::StrafeSettings)?, + rocket:self.rocket.map(Into::into), + jump:self.jump.map(TryInto::try_into).transpose().map_err(StyleModifierError::JumpSettings)?, + walk:self.walk.map(Into::into), + ladder:self.ladder.map(Into::into), + swim:self.swim.map(Into::into), + gravity:strafesnet_common::integer::vec3::raw_array(self.gravity), + hitbox:self.hitbox.into(), + camera_offset:strafesnet_common::integer::vec3::raw_array(self.camera_offset), + mass:strafesnet_common::integer::Planar64::raw(self.mass), + }) + } +} +impl From for StyleModifiers{ + fn from(value:strafesnet_common::gameplay_style::StyleModifiers)->Self{ + let header= + flag(value.strafe.is_some(),StyleModifiers::STRAFE) + |flag(value.rocket.is_some(),StyleModifiers::ROCKET) + |flag(value.jump.is_some(),StyleModifiers::JUMP) + |flag(value.walk.is_some(),StyleModifiers::WALK) + |flag(value.ladder.is_some(),StyleModifiers::LADDER) + |flag(value.swim.is_some(),StyleModifiers::SWIM); + Self{ + header, + controls_mask:value.controls_mask.bits(), + controls_mask_state:value.controls_mask_state.bits(), + strafe:value.strafe.map(Into::into), + rocket:value.rocket.map(Into::into), + jump:value.jump.map(Into::into), + walk:value.walk.map(Into::into), + ladder:value.ladder.map(Into::into), + swim:value.swim.map(Into::into), + gravity:value.gravity.map(|t|t.to_raw()).to_array(), + hitbox:value.hitbox.into(), + camera_offset:value.camera_offset.map(|t|t.to_raw()).to_array(), + mass:value.mass.to_raw(), + } + } +} + +#[binrw::binrw] +#[brw(little,repr=u8)] +pub enum JumpCalculation{ + Max, + BoostThenJump, + JumpThenBoost, +} +impl Into for JumpCalculation{ + fn into(self)->strafesnet_common::gameplay_style::JumpCalculation{ + match self{ + JumpCalculation::Max=>strafesnet_common::gameplay_style::JumpCalculation::Max, + JumpCalculation::BoostThenJump=>strafesnet_common::gameplay_style::JumpCalculation::BoostThenJump, + JumpCalculation::JumpThenBoost=>strafesnet_common::gameplay_style::JumpCalculation::JumpThenBoost, + } + } +} +impl From for JumpCalculation{ + fn from(value:strafesnet_common::gameplay_style::JumpCalculation)->Self{ + match value{ + strafesnet_common::gameplay_style::JumpCalculation::Max=>JumpCalculation::Max, + strafesnet_common::gameplay_style::JumpCalculation::BoostThenJump=>JumpCalculation::BoostThenJump, + strafesnet_common::gameplay_style::JumpCalculation::JumpThenBoost=>JumpCalculation::JumpThenBoost, + } + } +} + +pub enum JumpImpulse{ + Time(Time), + Height(Planar64), + Linear(Planar64), + Energy(Planar64), +} +impl Into for JumpImpulse{ + fn into(self)->strafesnet_common::gameplay_style::JumpImpulse{ + match self{ + JumpImpulse::Time(time)=>strafesnet_common::gameplay_style::JumpImpulse::Time(strafesnet_common::integer::Time::raw(time)), + JumpImpulse::Height(height)=>strafesnet_common::gameplay_style::JumpImpulse::Height(strafesnet_common::integer::Planar64::raw(height)), + JumpImpulse::Linear(deltav)=>strafesnet_common::gameplay_style::JumpImpulse::Linear(strafesnet_common::integer::Planar64::raw(deltav)), + JumpImpulse::Energy(energy)=>strafesnet_common::gameplay_style::JumpImpulse::Energy(strafesnet_common::integer::Planar64::raw(energy)), + } + } +} +impl From for JumpImpulse{ + fn from(value:strafesnet_common::gameplay_style::JumpImpulse)->Self{ + match value{ + strafesnet_common::gameplay_style::JumpImpulse::Time(time)=>JumpImpulse::Time(time.get()), + strafesnet_common::gameplay_style::JumpImpulse::Height(height)=>JumpImpulse::Height(height.to_raw()), + strafesnet_common::gameplay_style::JumpImpulse::Linear(deltav)=>JumpImpulse::Linear(deltav.to_raw()), + strafesnet_common::gameplay_style::JumpImpulse::Energy(energy)=>JumpImpulse::Energy(energy.to_raw()), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct ControlsActivation{ + controls_mask:Controls, + controls_intersects:Controls, + controls_contains:Controls, +} +impl TryInto for ControlsActivation{ + type Error=ControlsError; + fn try_into(self)->Result{ + Ok(strafesnet_common::gameplay_style::ControlsActivation{ + controls_mask:strafesnet_common::controls_bitflag::Controls::from_bits(self.controls_mask).ok_or(ControlsError::UnknownBits)?, + controls_intersects:strafesnet_common::controls_bitflag::Controls::from_bits(self.controls_intersects).ok_or(ControlsError::UnknownBits)?, + controls_contains:strafesnet_common::controls_bitflag::Controls::from_bits(self.controls_contains).ok_or(ControlsError::UnknownBits)?, + }) + } +} +impl From for ControlsActivation{ + fn from(value:strafesnet_common::gameplay_style::ControlsActivation)->Self{ + Self{ + controls_mask:value.controls_mask.bits(), + controls_intersects:value.controls_intersects.bits(), + controls_contains:value.controls_contains.bits(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct StrafeSettings{ + header:u8, + enable:ControlsActivation, + mv:Planar64, + #[br(if(header&Self::AIR_ACCEL_LIMIT!=0))] + air_accel_limit:Option, + tick_rate:Ratio64, +} +impl StrafeSettings{ + const AIR_ACCEL_LIMIT:u8=1<<0; +} +#[derive(Debug)] +pub enum StrafeSettingsError{ + Ratio(super::integer::RatioError), + Controls(ControlsError), +} +impl std::fmt::Display for StrafeSettingsError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for StrafeSettingsError{} +impl TryInto for StrafeSettings{ + type Error=StrafeSettingsError; + fn try_into(self)->Result{ + Ok(strafesnet_common::gameplay_style::StrafeSettings{ + enable:self.enable.try_into().map_err(StrafeSettingsError::Controls)?, + mv:strafesnet_common::integer::Planar64::raw(self.mv), + air_accel_limit:self.air_accel_limit.map(strafesnet_common::integer::Planar64::raw), + tick_rate:self.tick_rate.try_into().map_err(StrafeSettingsError::Ratio)?, + }) + } +} +impl From for StrafeSettings{ + fn from(value:strafesnet_common::gameplay_style::StrafeSettings)->Self{ + let header=flag(value.air_accel_limit.is_some(),StrafeSettings::AIR_ACCEL_LIMIT); + Self{ + header, + enable:value.enable.into(), + mv:value.mv.to_raw(), + air_accel_limit:value.air_accel_limit.map(|a|a.to_raw()), + tick_rate:value.tick_rate.into(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct PropulsionSettings{ + magnitude:Planar64, +} +impl Into for PropulsionSettings{ + fn into(self)->strafesnet_common::gameplay_style::PropulsionSettings{ + strafesnet_common::gameplay_style::PropulsionSettings{ + magnitude:strafesnet_common::integer::Planar64::raw(self.magnitude) + } + } +} +impl From for PropulsionSettings{ + fn from(value:strafesnet_common::gameplay_style::PropulsionSettings)->Self{ + Self{ + magnitude:value.magnitude.to_raw(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct JumpSettings{ + header:u8, + impulse:i64, +} +impl JumpSettings{ + const IMPULSE:u8=0b00011; + const CALCULATION:u8=0b01100; + const LIMIT_MINIMUM:u8=0b10000; + const fn impulse(&self)->Option{ + match self.header&Self::IMPULSE{ + 0=>Some(strafesnet_common::gameplay_style::JumpImpulse::Time(strafesnet_common::integer::Time::raw(self.impulse))), + 1=>Some(strafesnet_common::gameplay_style::JumpImpulse::Height(strafesnet_common::integer::Planar64::raw(self.impulse))), + 2=>Some(strafesnet_common::gameplay_style::JumpImpulse::Linear(strafesnet_common::integer::Planar64::raw(self.impulse))), + 3=>Some(strafesnet_common::gameplay_style::JumpImpulse::Energy(strafesnet_common::integer::Planar64::raw(self.impulse))), + _=>None, + } + } + const fn calculation(&self)->Option{ + match (self.header&Self::CALCULATION)>>2{ + 0=>Some(strafesnet_common::gameplay_style::JumpCalculation::Max), + 1=>Some(strafesnet_common::gameplay_style::JumpCalculation::JumpThenBoost), + 2=>Some(strafesnet_common::gameplay_style::JumpCalculation::BoostThenJump), + _=>None, + } + } + const fn limit_minimum(&self)->bool{ + self.header&Self::LIMIT_MINIMUM!=0 + } +} +#[derive(Debug)] +pub enum JumpSettingsError{ + InvalidImpulseDiscriminant, + InvalidCalculationDiscriminant, +} +impl TryInto for JumpSettings{ + type Error=JumpSettingsError; + fn try_into(self)->Result{ + Ok(strafesnet_common::gameplay_style::JumpSettings{ + impulse:self.impulse().ok_or(JumpSettingsError::InvalidImpulseDiscriminant)?, + calculation:self.calculation().ok_or(JumpSettingsError::InvalidCalculationDiscriminant)?, + limit_minimum:self.limit_minimum(), + }) + } +} +impl From for JumpSettings{ + fn from(value:strafesnet_common::gameplay_style::JumpSettings)->Self{ + let (impulse,impulse_header)=match value.impulse{ + strafesnet_common::gameplay_style::JumpImpulse::Time(impulse)=>(impulse.get(),0), + strafesnet_common::gameplay_style::JumpImpulse::Height(impulse)=>(impulse.to_raw(),1), + strafesnet_common::gameplay_style::JumpImpulse::Linear(impulse)=>(impulse.to_raw(),2), + strafesnet_common::gameplay_style::JumpImpulse::Energy(impulse)=>(impulse.to_raw(),3), + }; + let calculation_header=match value.calculation{ + strafesnet_common::gameplay_style::JumpCalculation::Max=>0, + strafesnet_common::gameplay_style::JumpCalculation::JumpThenBoost=>1, + strafesnet_common::gameplay_style::JumpCalculation::BoostThenJump=>2, + }; + let header= + impulse_header + |(calculation_header<<2) + |((value.limit_minimum as u8)<<4); + Self{ + header, + impulse, + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct AccelerateSettings{ + accel:Planar64, + topspeed:Planar64, +} +impl Into for AccelerateSettings{ + fn into(self)->strafesnet_common::gameplay_style::AccelerateSettings{ + strafesnet_common::gameplay_style::AccelerateSettings{ + accel:strafesnet_common::integer::Planar64::raw(self.accel), + topspeed:strafesnet_common::integer::Planar64::raw(self.topspeed), + } + } +} +impl From for AccelerateSettings{ + fn from(value:strafesnet_common::gameplay_style::AccelerateSettings)->Self{ + Self{ + accel:value.accel.to_raw(), + topspeed:value.topspeed.to_raw(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct WalkSettings{ + accelerate:AccelerateSettings, + static_friction:Planar64, + kinetic_friction:Planar64, + surf_dot:Planar64, +} +impl Into for WalkSettings{ + fn into(self)->strafesnet_common::gameplay_style::WalkSettings{ + strafesnet_common::gameplay_style::WalkSettings{ + accelerate:self.accelerate.into(), + static_friction:strafesnet_common::integer::Planar64::raw(self.static_friction), + kinetic_friction:strafesnet_common::integer::Planar64::raw(self.kinetic_friction), + surf_dot:strafesnet_common::integer::Planar64::raw(self.surf_dot), + } + } +} +impl From for WalkSettings{ + fn from(value:strafesnet_common::gameplay_style::WalkSettings)->Self{ + Self{ + accelerate:value.accelerate.into(), + static_friction:value.static_friction.to_raw(), + kinetic_friction:value.kinetic_friction.to_raw(), + surf_dot:value.surf_dot.to_raw(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct LadderSettings{ + accelerate:AccelerateSettings, + dot:Planar64, +} +impl Into for LadderSettings{ + fn into(self)->strafesnet_common::gameplay_style::LadderSettings{ + strafesnet_common::gameplay_style::LadderSettings{ + accelerate:self.accelerate.into(), + dot:strafesnet_common::integer::Planar64::raw(self.dot), + } + } +} +impl From for LadderSettings{ + fn from(value:strafesnet_common::gameplay_style::LadderSettings)->Self{ + Self{ + accelerate:value.accelerate.into(), + dot:value.dot.to_raw(), + } + } +} + +#[binrw::binrw] +#[brw(little,repr=u8)] +pub enum HitboxMesh{ + Box, + Cylinder, +} +impl Into for HitboxMesh{ + fn into(self)->strafesnet_common::gameplay_style::HitboxMesh{ + match self{ + HitboxMesh::Box=>strafesnet_common::gameplay_style::HitboxMesh::Box, + HitboxMesh::Cylinder=>strafesnet_common::gameplay_style::HitboxMesh::Cylinder, + } + } +} +impl From for HitboxMesh{ + fn from(value:strafesnet_common::gameplay_style::HitboxMesh)->Self{ + match value{ + strafesnet_common::gameplay_style::HitboxMesh::Box=>HitboxMesh::Box, + strafesnet_common::gameplay_style::HitboxMesh::Cylinder=>HitboxMesh::Cylinder, + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Hitbox{ + pub halfsize:Planar64Vec3, + pub mesh:HitboxMesh, +} +impl Into for Hitbox{ + fn into(self)->strafesnet_common::gameplay_style::Hitbox{ + strafesnet_common::gameplay_style::Hitbox{ + halfsize:strafesnet_common::integer::vec3::raw_array(self.halfsize), + mesh:self.mesh.into(), + } + } +} +impl From for Hitbox{ + fn from(value:strafesnet_common::gameplay_style::Hitbox)->Self{ + Self{ + halfsize:value.halfsize.map(|t|t.to_raw()).to_array(), + mesh:value.mesh.into(), + } + } +} diff --git a/lib/snf/src/newtypes/integer.rs b/lib/snf/src/newtypes/integer.rs new file mode 100644 index 0000000..0eb7711 --- /dev/null +++ b/lib/snf/src/newtypes/integer.rs @@ -0,0 +1,46 @@ +pub type Time=i64; + +#[binrw::binrw] +#[brw(little)] +pub struct Ratio64{ + num:i64, + den:u64, +} +#[derive(Debug)] +pub enum RatioError{ + ZeroDenominator, +} +impl std::fmt::Display for RatioError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for RatioError{} +impl TryInto for Ratio64{ + type Error=RatioError; + fn try_into(self)->Result{ + strafesnet_common::integer::Ratio64::new(self.num,self.den) + .ok_or(RatioError::ZeroDenominator) + } +} +impl From for Ratio64{ + fn from(value:strafesnet_common::integer::Ratio64)->Self{ + Self{ + num:value.num(), + den:value.den(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Ratio64Vec2{ + pub x:Ratio64, + pub y:Ratio64, +} + +pub type Angle32=i32; +pub type Planar64=i64; +pub type Planar64Vec3=[i64;3]; +pub type Planar64Mat3=[i64;9]; +pub type Planar64Affine3=[i64;12]; diff --git a/lib/snf/src/newtypes/model.rs b/lib/snf/src/newtypes/model.rs new file mode 100644 index 0000000..6d3ce51 --- /dev/null +++ b/lib/snf/src/newtypes/model.rs @@ -0,0 +1,276 @@ +use super::common::flag; +use strafesnet_common::model::PolygonIter; + +use super::integer::{Planar64Vec3,Planar64Affine3}; + +pub type TextureCoordinate=[f32;2]; +pub type Color4=[f32;4]; + +#[binrw::binrw] +#[brw(little)] +pub struct IndexedVertex{ + pub pos:u32, + pub tex:u32, + pub normal:u32, + pub color:u32, +} +impl From for IndexedVertex{ + fn from(value:strafesnet_common::model::IndexedVertex)->Self{ + Self{ + pos:value.pos.get(), + tex:value.tex.get(), + normal:value.normal.get(), + color:value.color.get(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Polygon{ + pub count:u32, + #[br(count=count)] + pub vertices:Vec, +} +#[binrw::binrw] +#[brw(little)] +pub struct PolygonGroup{ + pub count:u32, + #[br(count=count)] + pub polys:Vec, +} +impl From for PolygonGroup{ + fn from(value:strafesnet_common::model::PolygonGroup)->Self{ + match value{ + strafesnet_common::model::PolygonGroup::PolygonList(polygon_list)=>{ + let polys:Vec=polygon_list.polys().map(|poly|{ + let vertices:Vec=poly.iter().map(|vert|vert.get()).collect(); + Polygon{ + count:vertices.len() as u32, + vertices, + } + }).collect(); + Self{ + count:polys.len() as u32, + polys, + } + } + } + } +} +#[binrw::binrw] +#[brw(little)] +pub struct RenderConfig{ + pub header:u8, + #[br(if(header&Self::TEXTURE!=0))] + pub texture:Option, +} +impl RenderConfig{ + const TEXTURE:u8=1<<0; +} +impl Into for RenderConfig{ + fn into(self)->strafesnet_common::model::RenderConfig{ + strafesnet_common::model::RenderConfig{ + texture:self.texture.map(strafesnet_common::model::TextureId::new), + } + } +} +impl From for RenderConfig{ + fn from(value:strafesnet_common::model::RenderConfig)->Self{ + let header=flag(value.texture.is_some(),RenderConfig::TEXTURE); + Self{ + header, + texture:value.texture.map(|texture_id|texture_id.get()), + } + } +} +#[binrw::binrw] +#[brw(little)] +pub struct IndexedGraphicsGroup{ + pub count:u32, + pub render:u32, + #[br(count=count)] + pub groups:Vec, +} +impl From for IndexedGraphicsGroup{ + fn from(value:strafesnet_common::model::IndexedGraphicsGroup)->Self{ + Self{ + count:value.groups.len() as u32, + render:value.render.get(), + groups:value.groups.into_iter().map(|group_id|group_id.get()).collect(), + } + } +} +#[binrw::binrw] +#[brw(little)] +pub struct IndexedPhysicsGroup{ + pub count:u32, + #[br(count=count)] + pub groups:Vec, +} +impl From for IndexedPhysicsGroup{ + fn from(value:strafesnet_common::model::IndexedPhysicsGroup)->Self{ + Self{ + count:value.groups.len() as u32, + groups:value.groups.into_iter().map(|group_id|group_id.get()).collect(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct MeshHeader{ + pub unique_pos:u32, + pub unique_normal:u32, + pub unique_tex:u32, + pub unique_color:u32, + pub unique_vertices:u32, + pub polygon_groups:u32, + pub graphics_groups:u32, + pub physics_groups:u32, +} +#[binrw::binrw] +#[brw(little)] +pub struct Mesh{ + pub header:MeshHeader, + #[br(count=header.unique_pos)] + pub unique_pos:Vec, + #[br(count=header.unique_normal)] + pub unique_normal:Vec, + #[br(count=header.unique_tex)] + pub unique_tex:Vec, + #[br(count=header.unique_color)] + pub unique_color:Vec, + #[br(count=header.unique_vertices)] + pub unique_vertices:Vec, + #[br(count=header.polygon_groups)] + pub polygon_groups:Vec, + #[br(count=header.graphics_groups)] + pub graphics_groups:Vec, + #[br(count=header.physics_groups)] + pub physics_groups:Vec, +} +impl Into for Mesh{ + fn into(self)->strafesnet_common::model::Mesh{ + strafesnet_common::model::Mesh{ + unique_pos:self.unique_pos.into_iter().map(strafesnet_common::integer::vec3::raw_array).collect(), + unique_normal:self.unique_normal.into_iter().map(strafesnet_common::integer::vec3::raw_array).collect(), + unique_tex:self.unique_tex.into_iter().map(strafesnet_common::model::TextureCoordinate::from_array).collect(), + unique_color:self.unique_color.into_iter().map(strafesnet_common::model::Color4::from_array).collect(), + unique_vertices:self.unique_vertices.into_iter().map(|vert|strafesnet_common::model::IndexedVertex{ + pos:strafesnet_common::model::PositionId::new(vert.pos), + tex:strafesnet_common::model::TextureCoordinateId::new(vert.tex), + normal:strafesnet_common::model::NormalId::new(vert.normal), + color:strafesnet_common::model::ColorId::new(vert.color), + }).collect(), + polygon_groups:self.polygon_groups.into_iter().map(|group| + strafesnet_common::model::PolygonGroup::PolygonList( + strafesnet_common::model::PolygonList::new( + group.polys.into_iter().map(|vert| + vert.vertices.into_iter().map(strafesnet_common::model::VertexId::new).collect() + ).collect() + ) + ) + ).collect(), + graphics_groups:self.graphics_groups.into_iter().map(|group| + strafesnet_common::model::IndexedGraphicsGroup{ + render:strafesnet_common::model::RenderConfigId::new(group.render), + groups:group.groups.into_iter().map(strafesnet_common::model::PolygonGroupId::new).collect(), + } + ).collect(), + physics_groups:self.physics_groups.into_iter().map(|group| + strafesnet_common::model::IndexedPhysicsGroup{ + groups:group.groups.into_iter().map(strafesnet_common::model::PolygonGroupId::new).collect(), + } + ).collect(), + } + } +} +impl From for Mesh{ + fn from(value:strafesnet_common::model::Mesh)->Self{ + Self{ + header:MeshHeader{ + unique_pos:value.unique_pos.len() as u32, + unique_normal:value.unique_normal.len() as u32, + unique_tex:value.unique_tex.len() as u32, + unique_color:value.unique_color.len() as u32, + unique_vertices:value.unique_vertices.len() as u32, + polygon_groups:value.polygon_groups.len() as u32, + graphics_groups:value.graphics_groups.len() as u32, + physics_groups:value.physics_groups.len() as u32, + }, + unique_pos:value.unique_pos.into_iter() + .map(|pos|pos.map(|t|t.to_raw()).to_array()) + .collect(), + unique_normal:value.unique_normal.into_iter() + .map(|normal|normal.map(|t|t.to_raw()).to_array()) + .collect(), + unique_tex:value.unique_tex.into_iter() + .map(|tex|tex.to_array()) + .collect(), + unique_color:value.unique_color.into_iter() + .map(|color|color.to_array()) + .collect(), + unique_vertices:value.unique_vertices.into_iter() + .map(Into::into) + .collect(), + polygon_groups:value.polygon_groups.into_iter() + .map(Into::into) + .collect(), + graphics_groups:value.graphics_groups.into_iter() + .map(Into::into) + .collect(), + physics_groups:value.physics_groups.into_iter() + .map(Into::into) + .collect(), + } + } +} + +#[binrw::binrw] +#[brw(little)] +pub struct Model{ + pub mesh:u32, + pub attributes:u32, + pub color:Color4, + pub transform:Planar64Affine3, +} +impl Into for Model{ + fn into(self)->strafesnet_common::model::Model{ + let [_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_a,_b]=self.transform; + strafesnet_common::model::Model{ + mesh:strafesnet_common::model::MeshId::new(self.mesh), + attributes:strafesnet_common::gameplay_attributes::CollisionAttributesId::new(self.attributes), + color:strafesnet_common::model::Color4::from_array(self.color), + transform:strafesnet_common::integer::Planar64Affine3::new( + strafesnet_common::integer::Planar64Mat3::from_cols([ + strafesnet_common::integer::vec3::raw_xyz(_0,_1,_2), + strafesnet_common::integer::vec3::raw_xyz(_3,_4,_5), + strafesnet_common::integer::vec3::raw_xyz(_6,_7,_8) + ]), + strafesnet_common::integer::vec3::raw_xyz(_9,_a,_b) + ), + } + } +} +impl From for Model{ + fn from(value:strafesnet_common::model::Model)->Self{ + let ( + [_0,_1,_2], + [_3,_4,_5], + [_6,_7,_8], + [_9,_a,_b] + )=( + value.transform.matrix3.x_axis.map(|t|t.to_raw()).to_array(), + value.transform.matrix3.y_axis.map(|t|t.to_raw()).to_array(), + value.transform.matrix3.z_axis.map(|t|t.to_raw()).to_array(), + value.transform.translation.map(|t|t.to_raw()).to_array() + ); + Self{ + mesh:value.mesh.get(), + attributes:value.attributes.get(), + color:value.color.to_array(), + transform:[_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_a,_b], + } + } +}