diff --git a/lib/roblox_bot_file/.gitignore b/lib/roblox_bot_file/.gitignore new file mode 100644 index 0000000..51124b9 --- /dev/null +++ b/lib/roblox_bot_file/.gitignore @@ -0,0 +1,2 @@ +/target +/files diff --git a/lib/roblox_bot_file/Cargo.lock b/lib/roblox_bot_file/Cargo.lock new file mode 100644 index 0000000..42ad11c --- /dev/null +++ b/lib/roblox_bot_file/Cargo.lock @@ -0,0 +1,100 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "binrw" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strafesnet_roblox_bot_file" +version = "0.2.0" +dependencies = [ + "binrw", + "bitflags", +] + +[[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 = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" diff --git a/lib/roblox_bot_file/Cargo.toml b/lib/roblox_bot_file/Cargo.toml new file mode 100644 index 0000000..90803cf --- /dev/null +++ b/lib/roblox_bot_file/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "strafesnet_roblox_bot_file" +version = "0.2.0" +edition = "2021" + +[dependencies] +binrw = "0.14.1" +bitflags = "2.6.0" diff --git a/lib/roblox_bot_file/LICENSE-APACHE b/lib/roblox_bot_file/LICENSE-APACHE new file mode 100644 index 0000000..a7e77cb --- /dev/null +++ b/lib/roblox_bot_file/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/roblox_bot_file/LICENSE-MIT b/lib/roblox_bot_file/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/lib/roblox_bot_file/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/roblox_bot_file/README.md b/lib/roblox_bot_file/README.md new file mode 100644 index 0000000..ebdf7a7 --- /dev/null +++ b/lib/roblox_bot_file/README.md @@ -0,0 +1,46 @@ +Roblox Bhop/Surf Bot File Format +================================ + +## Example + +```rust +use strafesnet_roblox_bot_file::{File,TimedBlockId}; + +let file=std::fs::File::open("bot_file")?; +let input=std::io::BufReader::new(file); +let mut bot_file=File::new(input)?; + +// read the whole file +let block=bot_file.read_all()?; + +// or do data streaming block by block +for &TimedBlockId{time,block_id} in &bot_file.header.offline_blocks_timeline{ + // header is immutably borrowed + // while data is mutably borrowed + let block_info=bot_file.header.block_info(block_id)?; + let block=bot_file.data.read_block_info(block_info)?; + // offline blocks include the following event types: + // World, Gravity, Run, Camera, Setting +} +for &TimedBlockId{time,block_id} in &bot_file.header.realtime_blocks_timeline{ + let block_info=bot_file.header.block_info(block_id)?; + let block=bot_file.data.read_block_info(block_info)?; + // realtime blocks include the following event types: + // Input, Output, Sound +} +``` + +#### 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. + diff --git a/lib/roblox_bot_file/src/lib.rs b/lib/roblox_bot_file/src/lib.rs new file mode 100644 index 0000000..cc08a0d --- /dev/null +++ b/lib/roblox_bot_file/src/lib.rs @@ -0,0 +1,4 @@ +pub mod v0; + +#[cfg(test)] +mod tests; diff --git a/lib/roblox_bot_file/src/tests.rs b/lib/roblox_bot_file/src/tests.rs new file mode 100644 index 0000000..d1d3f84 --- /dev/null +++ b/lib/roblox_bot_file/src/tests.rs @@ -0,0 +1,41 @@ +use crate::v0::TimedBlockId; + +#[test] +fn _1()->Result<(),crate::v0::Error>{ + let file=std::fs::File::open("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); + let input=std::io::BufReader::new(file); + let mut bot_file=crate::v0::File::new(input).unwrap(); + println!("header={:?}",bot_file.header); + for &TimedBlockId{time,block_id} in &bot_file.header.offline_blocks_timeline{ + println!("offline time={} block_id={:?}",time,block_id); + let block_info=bot_file.header.block_info(block_id)?; + let block=bot_file.data.read_block_info(block_info)?; + // offline blocks include the following event types: + // World, Gravity, Run, Camera, Setting + } + for &TimedBlockId{time,block_id} in &bot_file.header.realtime_blocks_timeline{ + println!("realtime time={} block_id={:?}",time,block_id); + let block_info=bot_file.header.block_info(block_id)?; + let block=bot_file.data.read_block_info(block_info)?; + // realtime blocks include the following event types: + // Input, Output, Sound + } + + Ok(()) +} + +#[test] +fn _2()->Result<(),crate::v0::Error>{ + let file=std::fs::File::open("files/bhop_marble_7cf33a64-7120-4514-b9fa-4fe29d9523d").unwrap(); + let input=std::io::BufReader::new(file); + + let t0=std::time::Instant::now(); + + let mut bot_file=crate::v0::File::new(input).unwrap(); + + let block=bot_file.read_all()?; + + println!("{:?}",t0.elapsed()); + + Ok(()) +} diff --git a/lib/roblox_bot_file/src/v0.rs b/lib/roblox_bot_file/src/v0.rs new file mode 100644 index 0000000..f448d70 --- /dev/null +++ b/lib/roblox_bot_file/src/v0.rs @@ -0,0 +1,611 @@ +use binrw::{binrw,BinReaderExt,io::TakeSeekExt}; + +// the bit chunks are deposited in reverse +fn read_trey_float(bits:u32)->f32{ + let s=bits&1; + let e=(bits>>1)&((1<<8)-1); + let m=(bits>>(1+8))&((1<<23)-1); + f32::from_bits(m|(e<<23)|(s<<31)) +} +fn read_trey_double(bits:u64)->f64{ + let s=bits&1; + let e=(bits>>1)&((1<<11)-1); + let m=(bits>>(1+11))&((1<<52)-1); + f64::from_bits(m|(e<<52)|(s<<63)) +} + +#[binrw] +#[brw(little)] +pub struct Vector2{ + #[br(map=read_trey_float)] + pub x:f32, + #[br(map=read_trey_float)] + pub y:f32, +} +#[binrw] +#[brw(little)] +pub struct Vector3{ + #[br(map=read_trey_float)] + pub x:f32, + #[br(map=read_trey_float)] + pub y:f32, + #[br(map=read_trey_float)] + pub z:f32, +} + +bitflags::bitflags!{ + pub struct GameControls:u32{ + const MoveForward=1<<0; + const MoveLeft=1<<1; + const MoveBack=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; + const Action1=1<<15; + const Action2=1<<16; + } +} +#[derive(Debug)] +pub struct GameControlsError; +impl std::fmt::Display for GameControlsError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for GameControlsError{} +impl GameControls{ + fn try_from_bits(bits:u32)->Result{ + Self::from_bits(bits).ok_or(GameControlsError) + } +} + +// input +#[binrw] +#[brw(little)] +pub struct InputEvent{ + #[br(try_map=GameControls::try_from_bits)] + #[bw(map=GameControls::bits)] + pub game_controls:GameControls, + pub mouse_pos:Vector2, +} +#[binrw] +#[brw(little)] +pub struct TimedInputEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:InputEvent, +} + +// output +bitflags::bitflags!{ + pub struct TickInfo:u32{ + const TickEnd=1<<0; + const Jump=1<<1; + const Strafe=1<<2; + const Touching=1<<3; + } +} +#[derive(Debug)] +pub struct TickInfoError; +impl std::fmt::Display for TickInfoError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for TickInfoError{} +impl TickInfo{ + fn try_from_bits(bits:u32)->Result{ + Self::from_bits(bits).ok_or(TickInfoError) + } +} +#[binrw] +#[brw(little)] +pub struct OutputEvent{ + #[br(try_map=TickInfo::try_from_bits)] + #[bw(map=TickInfo::bits)] + pub tick_info:TickInfo, + pub angles:Vector3, + pub position:Vector3, + pub velocity:Vector3, + pub acceleration:Vector3, +} +#[binrw] +#[brw(little)] +pub struct TimedOutputEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:OutputEvent, +} + +// sound +#[binrw] +#[brw(little)] +pub enum SoundType{ + #[brw(magic=101u32)] + TrackGround, + #[brw(magic=102u32)] + TrackLadder, + #[brw(magic=103u32)] + TrackWater, + #[brw(magic=104u32)] + TrackAir, + #[brw(magic=201u32)] + JumpGround, + #[brw(magic=202u32)] + JumpLadder, + #[brw(magic=301u32)] + SmashGround, + #[brw(magic=302u32)] + SmashWall, +} +#[binrw] +#[brw(little)] +pub struct SoundEvent{ + pub sound_type:SoundType, + /// Roblox enum + pub material:u32, +} +#[binrw] +#[brw(little)] +pub struct TimedSoundEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:SoundEvent, +} + +// world +#[binrw] +#[brw(little)] +pub struct WorldEventReset{ + pub position:Vector3, +} +#[binrw] +#[brw(little)] +pub struct WorldEventButton{ + #[br(pad_after=8)] + pub button_id:u32, +} +#[binrw] +#[brw(little)] +pub struct WorldEventSetTime{ + #[br(map=read_trey_double)] + #[br(pad_after=4)] + pub time:f64, +} +#[binrw] +#[brw(little)] +pub struct WorldEventSetPaused{ + #[br(map=|paused:u32|paused!=0)] + #[bw(map=|paused:&bool|*paused as u32)] + #[br(pad_after=8)] + pub paused:bool, +} +#[binrw] +#[brw(little)] +pub enum WorldEvent{ + #[brw(magic=0u32)] + Reset(WorldEventReset), + #[brw(magic=1u32)] + Button(WorldEventButton), + #[brw(magic=2u32)] + SetTime(WorldEventSetTime), + #[brw(magic=3u32)] + SetPaused(WorldEventSetPaused), +} +#[binrw] +#[brw(little)] +pub struct TimedWorldEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:WorldEvent, +} + +// gravity +#[binrw] +#[brw(little)] +pub struct GravityEvent{ + pub gravity:Vector3, +} +#[binrw] +#[brw(little)] +pub struct TimedGravityEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:GravityEvent, +} + +// run +#[binrw] +#[brw(little)] +pub enum RunEventType{ + #[brw(magic=0u32)] + Prepare, + #[brw(magic=1u32)] + Start, + #[brw(magic=2u32)] + Finish, + #[brw(magic=3u32)] + Clear, + #[brw(magic=4u32)] + Flag, + #[brw(magic=5u32)] + LoadState, + #[brw(magic=6u32)] + SaveState, +} +#[binrw] +#[brw(little)] +pub enum Mode{ + #[brw(magic=0i32)] + Main, + #[brw(magic=1i32)] + Bonus, + #[brw(magic=-1i32)] + All, + #[brw(magic=-2i32)] + Invalid, + #[brw(magic=-3i32)] + InProgress, +} +#[binrw] +#[brw(little)] +pub enum FlagReason{ + #[brw(magic=0u32)] + Anticheat, + #[brw(magic=1u32)] + StyleChange, + #[brw(magic=2u32)] + Clock, + #[brw(magic=3u32)] + Pause, + #[brw(magic=4u32)] + Flying, + #[brw(magic=5u32)] + Gravity, + #[brw(magic=6u32)] + Timescale, + #[brw(magic=7u32)] + Timetravel, + #[brw(magic=8u32)] + Teleport, + #[brw(magic=9u32)] + Practice, + // b"data" + #[brw(magic=1635017060u32)] + None, +} +#[binrw] +#[brw(little)] +pub struct RunEvent{ + pub run_event_type:RunEventType, + pub mode:Mode, + pub flag_reason:FlagReason, +} +#[binrw] +#[brw(little)] +pub struct TimedRunEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:RunEvent, +} + +// camera +#[binrw] +#[brw(little)] +pub enum CameraEventType{ + #[brw(magic=0u32)] + CameraPunch, + #[brw(magic=1u32)] + Transform, +} +#[binrw] +#[brw(little)] +pub struct CameraEvent{ + pub camera_event_type:CameraEventType, + pub value:Vector3, +} +#[binrw] +#[brw(little)] +pub struct TimedCameraEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:CameraEvent, +} + +// setting +#[binrw] +#[brw(little)] +pub enum SettingType{ + #[brw(magic=0u32)] + FieldOfView, + #[brw(magic=1u32)] + Sensitivity, + #[brw(magic=2u32)] + VerticalSensitivityMultiplier, + #[brw(magic=3u32)] + AbsoluteSensitivity, + #[brw(magic=4u32)] + TurnSpeed, +} +#[binrw] +#[brw(little)] +pub struct SettingEvent{ + pub setting_type:SettingType, + #[br(map=read_trey_double)] + pub value:f64, +} +#[binrw] +#[brw(little)] +pub struct TimedSettingEvent{ + #[br(map=read_trey_double)] + pub time:f64, + pub event:SettingEvent, +} + +#[derive(Default)] +pub struct Block{ + pub input_events:Vec, + pub output_events:Vec, + pub sound_events:Vec, + pub world_events:Vec, + pub gravity_events:Vec, + pub run_events:Vec, + pub camera_events:Vec, + pub setting_events:Vec, +} + +#[binrw] +#[brw(little)] +enum EventType{ + #[brw(magic=1u32)] + Input, + #[brw(magic=2u32)] + Output, + #[brw(magic=3u32)] + Sound, + #[brw(magic=4u32)] + World, + #[brw(magic=5u32)] + Gravity, + #[brw(magic=6u32)] + Run, + #[brw(magic=7u32)] + Camera, + #[brw(magic=8u32)] + Setting, +} +#[binrw] +#[brw(little)] +struct EventChunkHeader{ + event_type:EventType, + num_events:u32, +} + +// first time I've managed to write BinRead generics without this stupid T::Args<'a>:Required thing blocking me +fn read_data_into_events<'a,R:BinReaderExt,T>(data:&mut R,events:&mut Vec,num_events:usize)->binrw::BinResult<()> +where + T:binrw::BinRead, + T::Args<'a>:binrw::__private::Required, +{ + // there is only supposed to be at most one of each type of event chunk per block, so no need to amortize. + events.reserve_exact(num_events); + for _ in 0..num_events{ + events.push(data.read_le()?); + } + Ok(()) +} +fn read_data_into_events_amortized<'a,R:BinReaderExt,T>(data:&mut R,events:&mut Vec,num_events:usize)->binrw::BinResult<()> +where + T:binrw::BinRead, + T::Args<'a>:binrw::__private::Required, +{ + // this is used when reading multiple blocks into a single object, so amortize the allocation cost. + events.reserve(num_events); + for _ in 0..num_events{ + events.push(data.read_le()?); + } + Ok(()) +} + +impl Block{ + fn read(data:R)->binrw::BinResult{ + let mut block=Block::default(); + Block::read_into(data,&mut block)?; + Ok(block) + } + fn read_into(mut data:R,block:&mut Block)->binrw::BinResult<()>{ + // well... this looks error prone + while let Ok(event_chunk_header)=data.read_le::(){ + match event_chunk_header.event_type{ + EventType::Input=>read_data_into_events(&mut data,&mut block.input_events,event_chunk_header.num_events as usize)?, + EventType::Output=>read_data_into_events(&mut data,&mut block.output_events,event_chunk_header.num_events as usize)?, + EventType::Sound=>read_data_into_events(&mut data,&mut block.sound_events,event_chunk_header.num_events as usize)?, + EventType::World=>read_data_into_events(&mut data,&mut block.world_events,event_chunk_header.num_events as usize)?, + EventType::Gravity=>read_data_into_events(&mut data,&mut block.gravity_events,event_chunk_header.num_events as usize)?, + EventType::Run=>read_data_into_events(&mut data,&mut block.run_events,event_chunk_header.num_events as usize)?, + EventType::Camera=>read_data_into_events(&mut data,&mut block.camera_events,event_chunk_header.num_events as usize)?, + EventType::Setting=>read_data_into_events(&mut data,&mut block.setting_events,event_chunk_header.num_events as usize)?, + } + } + Ok(()) + } + fn read_into_amortized(mut data:R,block:&mut Block)->binrw::BinResult<()>{ + // sad code duplication + while let Ok(event_chunk_header)=data.read_le::(){ + match event_chunk_header.event_type{ + EventType::Input=>read_data_into_events_amortized(&mut data,&mut block.input_events,event_chunk_header.num_events as usize)?, + EventType::Output=>read_data_into_events_amortized(&mut data,&mut block.output_events,event_chunk_header.num_events as usize)?, + EventType::Sound=>read_data_into_events_amortized(&mut data,&mut block.sound_events,event_chunk_header.num_events as usize)?, + EventType::World=>read_data_into_events_amortized(&mut data,&mut block.world_events,event_chunk_header.num_events as usize)?, + EventType::Gravity=>read_data_into_events_amortized(&mut data,&mut block.gravity_events,event_chunk_header.num_events as usize)?, + EventType::Run=>read_data_into_events_amortized(&mut data,&mut block.run_events,event_chunk_header.num_events as usize)?, + EventType::Camera=>read_data_into_events_amortized(&mut data,&mut block.camera_events,event_chunk_header.num_events as usize)?, + EventType::Setting=>read_data_into_events_amortized(&mut data,&mut block.setting_events,event_chunk_header.num_events as usize)?, + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum Error{ + InvalidBlockId(BlockId), + Seek(std::io::Error), + InvalidData(binrw::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{} + +#[binrw] +#[brw(little)] +#[derive(Debug,Clone,Copy)] +pub struct BlockId(#[br(map=|i:u32|i-1)]u32); +#[binrw] +#[brw(little)] +#[derive(Debug,Clone,Copy)] +pub struct BlockPosition(#[br(map=|i:u32|i-1)]u32); + +#[binrw] +#[brw(little)] +#[derive(Debug,Clone,Copy)] +pub struct TimedBlockId{ + #[br(map=read_trey_double)] + pub time:f64, + pub block_id:BlockId, +} +impl PartialEq for TimedBlockId{ + fn eq(&self,other:&Self)->bool{ + self.time.eq(&other.time) + } +} +impl PartialOrd for TimedBlockId{ + fn partial_cmp(&self,other:&Self)->Option{ + self.time.partial_cmp(&other.time) + } +} + +#[binrw] +#[brw(little)] +#[derive(Debug)] +pub struct FileHeader{ + #[brw(magic=b"qbot")] + pub file_version:u32, + pub num_offline_events:u32, + pub num_realtime_events:u32, + #[br(count=num_offline_events+num_realtime_events+1)] + pub block_positions:Vec, + #[br(count=num_offline_events)] + pub offline_blocks_timeline:Vec, + #[br(count=num_realtime_events)] + pub realtime_blocks_timeline:Vec, +} +pub struct BlockInfo{ + start:u32, + length:u32, +} +impl FileHeader{ + pub fn block_info(&self,block_id:BlockId)->Result{ + if self.block_positions.len() as u32<=block_id.0{ + return Err(Error::InvalidBlockId(block_id)); + } + let start=self.block_positions[block_id.0 as usize].0; + let end=self.block_positions[block_id.0 as usize+1].0; + Ok(BlockInfo{start,length:end-start}) + } +} + +struct MergeIter,It1:Iterator>{ + it0:It0, + it1:It1, + item0:Option, + item1:Option, +} +impl,It1:Iterator> MergeIter{ + fn new(mut it0:It0,mut it1:It1)->Self{ + Self{ + item0:it0.next(), + item1:it1.next(), + it0, + it1, + } + } +} +impl,It1:Iterator> Iterator for MergeIter{ + type Item=T; + fn next(&mut self)->Option{ + match (&self.item0,&self.item1){ + (None,None)=>None, + (Some(_),None)=>core::mem::replace(&mut self.item0,self.it0.next()), + (None,Some(_))=>core::mem::replace(&mut self.item1,self.it1.next()), + (Some(item0),Some(item1))=>match item0.partial_cmp(item1){ + Some(core::cmp::Ordering::Less) + |Some(core::cmp::Ordering::Equal) + |None + =>core::mem::replace(&mut self.item0,self.it0.next()), + Some(core::cmp::Ordering::Greater) + =>core::mem::replace(&mut self.item1,self.it1.next()), + }, + } + } +} + +pub struct File{ + pub header:FileHeader, + pub data:FileData, +} +impl File{ + pub fn new(mut data:R)->Result,binrw::Error>{ + Ok(File{ + header:data.read_le()?, + data:FileData{data}, + }) + } + pub fn read_all(&mut self)->Result{ + let block_iter=MergeIter::new( + self.header.offline_blocks_timeline.iter(), + self.header.realtime_blocks_timeline.iter(), + ); + let mut big_block=Block::default(); + for &TimedBlockId{time:_,block_id} in block_iter{ + let block_info=self.header.block_info(block_id)?; + self.data.read_block_info_into_block(block_info,&mut big_block)?; + } + Ok(big_block) + } +} + +pub struct FileData{ + data:R, +} +impl FileData{ + fn data_mut(&mut self)->&mut R{ + &mut self.data + } + fn block_reader(&mut self,block_info:BlockInfo)->Result,Error>{ + self.data.seek(std::io::SeekFrom::Start(block_info.start as u64)).map_err(Error::Seek)?; + Ok(self.data_mut().take_seek(block_info.length as u64)) + } + pub fn read_block_info(&mut self,block_info:BlockInfo)->Result{ + let data=self.block_reader(block_info)?; + let block=Block::read(data).map_err(Error::InvalidData)?; + Ok(block) + } + pub fn read_block_info_into_block(&mut self,block_info:BlockInfo,block:&mut Block)->Result<(),Error>{ + let data=self.block_reader(block_info)?; + Block::read_into_amortized(data,block).map_err(Error::InvalidData)?; + Ok(()) + } +}