diff --git a/Cargo.toml b/Cargo.toml index a007e09e..56ef9bb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,6 @@ hashbrown = "0.15.0" tinyvec = "1.8.0" dashmap = "6.1.0" uuid = { version = "1.1", features = ["v4", "v3", "serde"] } -whirlwind = "0.1.1" # Macros lazy_static = "1.5.0" @@ -164,12 +163,12 @@ libflate = "2.1.0" flate2 = { version = "1.0.33", features = ["zlib"], default-features = false } zstd = { version = "0.13.2" } brotli = "7.0.0" -lzzzz = "1.1.0" +lzzzz = "2.0.0" yazi = "0.2.0" -bzip2 = "0.4.1" +bzip2 = "0.5.0" # Database -heed = "0.20.5" +heed = "0.21.0" moka = "0.12.8" # CLI @@ -183,7 +182,6 @@ page_size = "0.6.0" regex = "1.11.1" # I/O -tempfile = "3.12.0" memmap2 = "0.9.5" # Benchmarking diff --git a/scripts/new_packet.py b/scripts/new_packet.py index 0f493608..be28b21c 100644 --- a/scripts/new_packet.py +++ b/scripts/new_packet.py @@ -8,7 +8,7 @@ use std::sync::Arc; #[derive(NetDecode)] -#[packet(packet_id = ++id++, state = "play")] +#[packet(packet_id = "++id++", state = "play")] pub struct ++name++ { } @@ -24,7 +24,7 @@ use std::io::Write; #[derive(NetEncode)] -#[packet(packet_id = ++id++)] +#[packet(packet_id = "++id++")] pub struct ++name++ {} """ @@ -50,8 +50,8 @@ def to_camel_case(string) -> str: packet_name = input("Packet name: ") packets_dir = os.path.join(os.path.join(os.path.dirname(__file__), ".."), "src/lib/net/src/packets") -packet_id = input("Packet ID (formatted like 0x01): ") -packet_id = packet_id[:-2] + packet_id[-2:].upper() +packet_id = input( + "Packet ID (formatted as snake case, look on https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol if you need to get the id): ") with open(f"{packets_dir}/{packet_type}/{to_snake_case(packet_name)}.rs", "x") as f: if packet_type == "incoming": diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 1451fff7..ec98ad79 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -35,6 +35,7 @@ async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } flate2 = { workspace = true } ctor = { workspace = true } +rand = { workspace = true } [[bin]] diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 1a3c9b7c..7a32d55c 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -1,5 +1,6 @@ #![feature(portable_simd)] #![forbid(unsafe_code)] +#![feature(random)] extern crate core; use crate::errors::BinaryError; diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index ccf82297..77a807dc 100644 --- a/src/bin/src/packet_handlers/login_process.rs +++ b/src/bin/src/packet_handlers/login_process.rs @@ -1,5 +1,6 @@ use ferrumc_config::statics::{get_global_config, get_whitelist}; use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::collisions::bounds::CollisionBounds; use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::position::Position; @@ -152,7 +153,18 @@ async fn handle_ack_finish_configuration( .add_component::(entity_id, Position::default())? .add_component::(entity_id, Rotation::default())? .add_component::(entity_id, OnGround::default())? - .add_component::(entity_id, ChunkReceiver::default())?; + .add_component::(entity_id, ChunkReceiver::default())? + .add_component::( + entity_id, + CollisionBounds { + x_offset_start: -0.3, + x_offset_end: 0.3, + y_offset_start: 0.0, + y_offset_end: 1.5, + z_offset_start: -0.3, + z_offset_end: 0.3, + }, + )?; let mut writer = state.universe.get_mut::(entity_id)?; diff --git a/src/bin/src/systems/chunk_fetcher.rs b/src/bin/src/systems/chunk_fetcher.rs index e219db8e..bb940923 100644 --- a/src/bin/src/systems/chunk_fetcher.rs +++ b/src/bin/src/systems/chunk_fetcher.rs @@ -2,7 +2,10 @@ use crate::errors::BinaryError; use crate::systems::definition::System; use async_trait::async_trait; use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::chunks::chunk_receiver::ChunkSendState::{Fetching, Sending}; use ferrumc_state::GlobalState; +use ferrumc_world::chunk_format::Chunk; +use ferrumc_world::vanilla_chunk_format::BlockData; use std::collections::HashMap; use std::sync::atomic::AtomicBool; use std::sync::Arc; @@ -13,6 +16,22 @@ pub struct ChunkFetcher { stop: AtomicBool, } +fn generate_chunk(x: i32, z: i32) -> Chunk { + let mut new_chunk = Chunk::new(x, z, "overworld".to_string()); + for y in 0..10 { + new_chunk + .set_section( + y, + BlockData { + name: "minecraft:stone".to_string(), + properties: None, + }, + ) + .unwrap() + } + new_chunk +} + impl ChunkFetcher { pub(crate) fn new() -> Self { Self { @@ -41,7 +60,7 @@ impl System for ChunkFetcher { let mut copied_chunks = HashMap::new(); for chunk in chunk_recv.needed_chunks.iter() { let (key, chunk) = chunk; - if chunk.is_none() { + if let Fetching = chunk { copied_chunks.insert(key.clone(), None); } } @@ -49,10 +68,23 @@ impl System for ChunkFetcher { }; // Fetch the chunks for (key, chunk) in copied_chunks.iter_mut() { - let fetched_chunk = - state.world.load_chunk(key.0, key.1, &key.2.clone()).await?; + let fetched_chunk = if state + .world + .chunk_exists(key.0, key.1, &key.2.clone()) + .await? + { + debug!("Chunk found, loading chunk"); + state.world.load_chunk(key.0, key.1, &key.2.clone()).await? + } else { + debug!("Chunk not found, creating new chunk"); + let new_chunk = generate_chunk(key.0, key.1); + + state.world.save_chunk(new_chunk.clone()).await?; + new_chunk + }; *chunk = Some(fetched_chunk); } + state.world.sync().await?; // Insert the fetched chunks back into the component { let Ok(mut chunk_recv) = state.universe.get_mut::(eid) @@ -60,8 +92,10 @@ impl System for ChunkFetcher { trace!("A player disconnected before we could get the ChunkReceiver"); return Ok(()); }; - for (key, chunk) in copied_chunks.iter() { - chunk_recv.needed_chunks.insert(key.clone(), chunk.clone()); + for (key, chunk) in copied_chunks { + if let Some(chunk) = chunk { + chunk_recv.needed_chunks.insert(key.clone(), Sending(chunk)); + } } } Ok(()) @@ -79,7 +113,7 @@ impl System for ChunkFetcher { } } } - tokio::time::sleep(std::time::Duration::from_millis(5)).await; + tokio::time::sleep(std::time::Duration::from_millis(45)).await; } } diff --git a/src/bin/src/systems/chunk_sender.rs b/src/bin/src/systems/chunk_sender.rs index 57a4a43a..2b48bc7a 100644 --- a/src/bin/src/systems/chunk_sender.rs +++ b/src/bin/src/systems/chunk_sender.rs @@ -1,6 +1,7 @@ use crate::systems::definition::System; use async_trait::async_trait; use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::chunks::chunk_receiver::ChunkSendState::{Sending, Sent}; use ferrumc_ecs::errors::ECSError; use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::chunk_and_light_data::ChunkAndLightData; @@ -55,7 +56,6 @@ impl System for ChunkSenderSystem { } // We can't delete from the map while iterating, so we collect the keys to drop // and then drop them after sending the chunks - let mut to_drop = Vec::new(); { let Ok(chunk_recv) = state.universe.get::(eid) else { trace!("A player disconnected before we could get the ChunkReceiver"); @@ -67,38 +67,27 @@ impl System for ChunkSenderSystem { centre_coords = (chunk.0, chunk.1); } } - let mut sent_chunks = 0; { - let Ok(mut chunk_recv) = state.universe.get_mut::(eid) - else { - trace!("A player disconnected before we could get the ChunkReceiver"); - return Ok(()); - }; - for possible_chunk in chunk_recv.needed_chunks.iter_mut() { - if let Some(chunk) = possible_chunk.1 { - let key = possible_chunk.0; - to_drop.push(key.clone()); - match ChunkAndLightData::from_chunk(&chunk.clone()) { + trace!("Getting chunk_recv 3 for sender"); + let mut chunk_recv = state + .universe + .get_mut::(eid) + .expect("ChunkReceiver not found"); + trace!("Got chunk_recv 3 for sender"); + for (_key, chunk) in chunk_recv.needed_chunks.iter_mut() { + if let Sending(confirmed_chunk) = chunk { + match ChunkAndLightData::from_chunk(&confirmed_chunk.clone()) { Ok(packet) => { packets.push(packet); - sent_chunks += 1; } Err(e) => { error!("Error sending chunk: {:?}", e); } } + *chunk = Sent; } } - } - { - let Ok(mut chunk_recv) = state.universe.get_mut::(eid) - else { - trace!("A player disconnected before we could get the ChunkReceiver"); - return Ok(()); - }; - for key in to_drop { - chunk_recv.needed_chunks.remove(&key); - } + chunk_recv.needed_chunks.retain(|_, v| v != &Sent); } { @@ -123,14 +112,17 @@ impl System for ChunkSenderSystem { { error!("Error sending chunk: {:?}", e); } + let mut count = 0; for packet in packets { if let Err(e) = conn.send_packet(packet, &NetEncodeOpts::WithLength) { error!("Error sending chunk: {:?}", e); + } else { + count += 1; } } if let Err(e) = conn.send_packet( ChunkBatchFinish { - batch_size: VarInt::new(sent_chunks), + batch_size: VarInt::new(count), }, &NetEncodeOpts::WithLength, ) { @@ -154,7 +146,7 @@ impl System for ChunkSenderSystem { } } - tokio::time::sleep(Duration::from_millis(5)).await; + tokio::time::sleep(Duration::from_millis(45)).await; } } diff --git a/src/lib/core/Cargo.toml b/src/lib/core/Cargo.toml index 435b049c..e01b29f0 100644 --- a/src/lib/core/Cargo.toml +++ b/src/lib/core/Cargo.toml @@ -12,3 +12,10 @@ dashmap = { workspace = true } ferrumc-world = { workspace = true } tracing = { workspace = true } log = "0.4.22" + +[dev-dependencies] +criterion = { workspace = true } + +[[bench]] +name = "core_bench" +harness = false diff --git a/src/lib/core/benches/collisions.rs b/src/lib/core/benches/collisions.rs new file mode 100644 index 00000000..3bac8865 --- /dev/null +++ b/src/lib/core/benches/collisions.rs @@ -0,0 +1,56 @@ +use criterion::{black_box, Criterion}; +use ferrumc_core::collisions::bounds::CollisionBounds; + +pub fn bench_collides(c: &mut Criterion) { + let mut g = c.benchmark_group("collisions"); + g.bench_function("Simple collides", |b| { + b.iter(|| { + let bounds1 = black_box(CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 2.0, + y_offset_start: 0.0, + y_offset_end: 2.0, + z_offset_start: 0.0, + z_offset_end: 2.0, + }); + let bounds2 = black_box(CollisionBounds { + x_offset_start: 1.0, + x_offset_end: 3.0, + y_offset_start: 1.0, + y_offset_end: 3.0, + z_offset_start: 1.0, + z_offset_end: 3.0, + }); + bounds1.collides( + black_box((0.0, 0.0, 0.0)), + &bounds2, + black_box((0.0, 0.0, 0.0)), + ); + }) + }); + g.bench_function("Complex collides", |b| { + b.iter(|| { + let bounds1 = black_box(CollisionBounds { + x_offset_start: 64.2, + x_offset_end: -8.4, + y_offset_start: -12.0, + y_offset_end: 16.3, + z_offset_start: 99.55, + z_offset_end: 100.999, + }); + let bounds2 = black_box(CollisionBounds { + x_offset_start: 5.0, + x_offset_end: 6.0, + y_offset_start: 1.0, + y_offset_end: 0.0, + z_offset_start: 2.0, + z_offset_end: 3.0, + }); + bounds1.collides( + black_box((12.0, 66.0, -5.0)), + &bounds2, + black_box((4444.0, -300.0, 0.1)), + ); + }) + }); +} diff --git a/src/lib/core/benches/core_bench.rs b/src/lib/core/benches/core_bench.rs new file mode 100644 index 00000000..b055d612 --- /dev/null +++ b/src/lib/core/benches/core_bench.rs @@ -0,0 +1,9 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +mod collisions; + +fn bench(c: &mut Criterion) { + collisions::bench_collides(c); +} + +criterion_group!(benches, bench); +criterion_main!(benches); diff --git a/src/lib/core/src/chunks/chunk_receiver.rs b/src/lib/core/src/chunks/chunk_receiver.rs index 90194960..33f868ca 100644 --- a/src/lib/core/src/chunks/chunk_receiver.rs +++ b/src/lib/core/src/chunks/chunk_receiver.rs @@ -1,13 +1,16 @@ use ferrumc_world::chunk_format::Chunk; use std::collections::{HashMap, HashSet}; +use std::sync::atomic::AtomicBool; use tokio::time::Instant; const VIEW_DISTANCE: i32 = 8; pub struct ChunkReceiver { - pub needed_chunks: HashMap<(i32, i32, String), Option>, + pub needed_chunks: HashMap<(i32, i32, String), ChunkSendState>, pub can_see: HashSet<(i32, i32, String)>, pub last_update: Instant, pub last_chunk: Option<(i32, i32, String)>, + pub chunks_per_tick: f32, + pub has_loaded: AtomicBool, } impl Default for ChunkReceiver { @@ -16,6 +19,13 @@ impl Default for ChunkReceiver { } } +#[derive(Clone, Eq, PartialEq)] +pub enum ChunkSendState { + Fetching, + Sending(Chunk), + Sent, +} + impl ChunkReceiver { pub fn new() -> Self { Self { @@ -23,6 +33,26 @@ impl ChunkReceiver { can_see: HashSet::new(), last_update: Instant::now(), last_chunk: None, + chunks_per_tick: 0.0, + has_loaded: AtomicBool::new(false), + } + } + + pub fn queue_chunk_resend(&mut self, x: i32, z: i32, dimension: String) { + if self.can_see.contains(&(x, z, dimension.clone())) { + let entry = self.needed_chunks.get_mut(&(x, z, dimension.clone())); + if let Some(entry) = entry { + *entry = ChunkSendState::Fetching; + } + } + } + + pub fn queue_from_chunk(&mut self, chunk: Chunk) { + let key = (chunk.x, chunk.z, chunk.dimension.clone()); + + if self.can_see.contains(&key) { + self.needed_chunks + .insert(key, ChunkSendState::Sending(chunk)); } } } @@ -35,7 +65,7 @@ impl ChunkReceiver { for z in last_chunk.1 - VIEW_DISTANCE..=last_chunk.1 + VIEW_DISTANCE { if !self.can_see.contains(&(x, z, last_chunk.2.clone())) { self.needed_chunks - .insert((x, z, last_chunk.2.clone()), None); + .insert((x, z, last_chunk.2.clone()), ChunkSendState::Fetching); } new_can_see.insert((x, z, last_chunk.2.clone())); } diff --git a/src/lib/core/src/collisions/bounds.rs b/src/lib/core/src/collisions/bounds.rs new file mode 100644 index 00000000..6a3d5330 --- /dev/null +++ b/src/lib/core/src/collisions/bounds.rs @@ -0,0 +1,170 @@ +pub struct CollisionBounds { + // Given a start position, where the bounding box starts on the x-axis. + pub x_offset_start: f64, + // Given a start position, where the bounding box ends on the x-axis. + pub x_offset_end: f64, + // Given a start position, where the bounding box starts on the y-axis. + pub y_offset_start: f64, + // Given a start position, where the bounding box ends on the y-axis. + pub y_offset_end: f64, + // Given a start position, where the bounding box starts on the z-axis. + pub z_offset_start: f64, + // Given a start position, where the bounding box ends on the z-axis. + pub z_offset_end: f64, +} + +impl Default for CollisionBounds { + fn default() -> Self { + CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 0.0, + y_offset_start: 0.0, + y_offset_end: 0.0, + z_offset_start: 0.0, + z_offset_end: 0.0, + } + } +} + +impl CollisionBounds { + #[inline] + pub fn collides( + &self, + own_pos: (f64, f64, f64), + other_bounds: &CollisionBounds, + other_pos: (f64, f64, f64), + ) -> bool { + let (own_x, own_y, own_z) = own_pos; + let (other_x, other_y, other_z) = other_pos; + + // Pre-calculate bounds + let own_x_start = own_x + self.x_offset_start; + let own_x_end = own_x + self.x_offset_end; + let own_y_start = own_y + self.y_offset_start; + let own_y_end = own_y + self.y_offset_end; + let own_z_start = own_z + self.z_offset_start; + let own_z_end = own_z + self.z_offset_end; + + let other_x_start = other_x + other_bounds.x_offset_start; + let other_x_end = other_x + other_bounds.x_offset_end; + let other_y_start = other_y + other_bounds.y_offset_start; + let other_y_end = other_y + other_bounds.y_offset_end; + let other_z_start = other_z + other_bounds.z_offset_start; + let other_z_end = other_z + other_bounds.z_offset_end; + + // Check collisions axis by axis + (own_x_start < other_x_end && own_x_end > other_x_start) + && (own_y_start < other_y_end && own_y_end > other_y_start) + && (own_z_start < other_z_end && own_z_end > other_z_start) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn collides_when_boxes_overlap() { + let bounds1 = CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 2.0, + y_offset_start: 0.0, + y_offset_end: 2.0, + z_offset_start: 0.0, + z_offset_end: 2.0, + }; + let bounds2 = CollisionBounds { + x_offset_start: 1.0, + x_offset_end: 3.0, + y_offset_start: 1.0, + y_offset_end: 3.0, + z_offset_start: 1.0, + z_offset_end: 3.0, + }; + assert!(bounds1.collides((0.0, 0.0, 0.0), &bounds2, (0.0, 0.0, 0.0))); + } + + #[test] + fn does_not_collide_when_boxes_do_not_overlap() { + let bounds1 = CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 1.0, + y_offset_start: 0.0, + y_offset_end: 1.0, + z_offset_start: 0.0, + z_offset_end: 1.0, + }; + let bounds2 = CollisionBounds { + x_offset_start: 2.0, + x_offset_end: 3.0, + y_offset_start: 2.0, + y_offset_end: 3.0, + z_offset_start: 2.0, + z_offset_end: 3.0, + }; + assert!(!bounds1.collides((0.0, 0.0, 0.0), &bounds2, (0.0, 0.0, 0.0))); + } + + #[test] + fn collides_when_boxes_touch_edges() { + let bounds1 = CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 1.0, + y_offset_start: 0.0, + y_offset_end: 1.0, + z_offset_start: 0.0, + z_offset_end: 1.0, + }; + let bounds2 = CollisionBounds { + x_offset_start: 1.0, + x_offset_end: 2.0, + y_offset_start: 1.0, + y_offset_end: 2.0, + z_offset_start: 1.0, + z_offset_end: 2.0, + }; + assert!(!bounds1.collides((0.0, 0.0, 0.0), &bounds2, (0.0, 0.0, 0.0))); + } + + #[test] + fn collides_when_one_box_inside_another() { + let bounds1 = CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 3.0, + y_offset_start: 0.0, + y_offset_end: 3.0, + z_offset_start: 0.0, + z_offset_end: 3.0, + }; + let bounds2 = CollisionBounds { + x_offset_start: 1.0, + x_offset_end: 2.0, + y_offset_start: 1.0, + y_offset_end: 2.0, + z_offset_start: 1.0, + z_offset_end: 2.0, + }; + assert!(bounds1.collides((0.0, 0.0, 0.0), &bounds2, (0.0, 0.0, 0.0))); + } + + #[test] + fn does_not_collide_when_positions_are_far_apart() { + let bounds1 = CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 1.0, + y_offset_start: 0.0, + y_offset_end: 1.0, + z_offset_start: 0.0, + z_offset_end: 1.0, + }; + let bounds2 = CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 1.0, + y_offset_start: 0.0, + y_offset_end: 1.0, + z_offset_start: 0.0, + z_offset_end: 1.0, + }; + assert!(!bounds1.collides((0.0, 0.0, 0.0), &bounds2, (10.0, 10.0, 10.0))); + } +} diff --git a/src/lib/core/src/collisions/mod.rs b/src/lib/core/src/collisions/mod.rs new file mode 100644 index 00000000..c919e66b --- /dev/null +++ b/src/lib/core/src/collisions/mod.rs @@ -0,0 +1 @@ +pub mod bounds; diff --git a/src/lib/core/src/lib.rs b/src/lib/core/src/lib.rs index 93d6b8d4..cb752268 100644 --- a/src/lib/core/src/lib.rs +++ b/src/lib/core/src/lib.rs @@ -2,6 +2,7 @@ pub mod errors; // Core structs/types. Usually used in ECS Components. pub mod chunks; +pub mod collisions; pub mod identity; pub mod state; pub mod transform; diff --git a/src/lib/derive_macros/src/net/packets/mod.rs b/src/lib/derive_macros/src/net/packets/mod.rs index 0bec0cff..44fbd9c8 100644 --- a/src/lib/derive_macros/src/net/packets/mod.rs +++ b/src/lib/derive_macros/src/net/packets/mod.rs @@ -118,7 +118,7 @@ pub fn bake_registry(input: TokenStream) -> TokenStream { .expect( "parse_packet_attribute failed\ \nPlease provide the packet_id and state fields in the #[packet(...)] attribute.\ - \nExample: #[packet(packet_id = 0x00, state = \"handshake\")]", + \nExample: #[packet(packet_id = \"example_packet\", state = \"handshake\")]", ); let struct_name = &item_struct.ident; diff --git a/src/lib/net/crates/codec/src/net_types/bitset.rs b/src/lib/net/crates/codec/src/net_types/bitset.rs index 2e7e8f12..45354cc1 100644 --- a/src/lib/net/crates/codec/src/net_types/bitset.rs +++ b/src/lib/net/crates/codec/src/net_types/bitset.rs @@ -9,7 +9,7 @@ pub struct BitSet(Vec); impl BitSet { pub fn new(size: usize) -> Self { - let num_blocks = (size + 63) / 64; + let num_blocks = size.div_ceil(64); Self(vec![0; num_blocks]) } diff --git a/src/lib/net/crates/codec/src/net_types/network_position.rs b/src/lib/net/crates/codec/src/net_types/network_position.rs index 4a745b0a..fc787a79 100644 --- a/src/lib/net/crates/codec/src/net_types/network_position.rs +++ b/src/lib/net/crates/codec/src/net_types/network_position.rs @@ -1,8 +1,9 @@ // I have no clue why it is saving i32 and i16. There is no precision. The actual player position is saved in f32. +use crate::decode::{NetDecode, NetDecodeOpts, NetDecodeResult}; use crate::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; use std::fmt::Display; -use std::io::Write; +use std::io::{Read, Write}; use tokio::io::AsyncWrite; /// The definition of a "Position" in the Minecraft protocol. @@ -47,10 +48,35 @@ impl NetEncode for NetworkPosition { Ok(()) } } + +impl NetDecode for NetworkPosition { + fn decode(reader: &mut R, _: &NetDecodeOpts) -> NetDecodeResult { + let mut buf = [0u8; 8]; + reader.read_exact(&mut buf)?; + Ok(NetworkPosition::from_u64(u64::from_be_bytes(buf))) + } +} + impl NetworkPosition { pub fn as_u64(&self) -> u64 { ((self.x as u64 & 0x3FFFFFF) << 38) | ((self.z as u64 & 0x3FFFFFF) << 12) | (self.y as u64 & 0xFFF) } + + pub fn from_u64(val: u64) -> Self { + let mut x = (val >> 38) as i32; + let mut y = (val << 52 >> 52) as i16; + let mut z = (val << 26 >> 38) as i32; + if x >= 1 << 25 { + x -= 1 << 26 + } + if y >= 1 << 11 { + y -= 1 << 12 + } + if z >= 1 << 25 { + z -= 1 << 26 + } + Self { x, y, z } + } } diff --git a/src/lib/net/crates/codec/src/net_types/var_int.rs b/src/lib/net/crates/codec/src/net_types/var_int.rs index 33231791..42010c81 100644 --- a/src/lib/net/crates/codec/src/net_types/var_int.rs +++ b/src/lib/net/crates/codec/src/net_types/var_int.rs @@ -8,7 +8,7 @@ use deepsize::DeepSizeOf; use std::io::{Read, Write}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -#[derive(Debug, Encode, Decode, Clone, DeepSizeOf)] +#[derive(Debug, Encode, Decode, Clone, DeepSizeOf, Eq)] pub struct VarInt { /// The value of the VarInt. pub val: i32, diff --git a/src/lib/net/src/errors.rs b/src/lib/net/src/errors.rs index d24d3684..e61863a0 100644 --- a/src/lib/net/src/errors.rs +++ b/src/lib/net/src/errors.rs @@ -38,11 +38,14 @@ pub enum NetError { #[error("Invalid State: {0}")] InvalidState(u8), - #[error("{0}")] + #[error("Packet error: {0}")] Packet(#[from] PacketError), - #[error("{0}")] + #[error("Chunk error: {0}")] Chunk(#[from] ChunkError), + + #[error("World error: {0}")] + World(#[from] ferrumc_world::errors::WorldError), } #[derive(Debug, Error)] diff --git a/src/lib/net/src/packets/incoming/chunk_batch_ack.rs b/src/lib/net/src/packets/incoming/chunk_batch_ack.rs new file mode 100644 index 00000000..11b997c1 --- /dev/null +++ b/src/lib/net/src/packets/incoming/chunk_batch_ack.rs @@ -0,0 +1,57 @@ +use crate::connection::StreamWriter; +use crate::packets::outgoing::synchronize_player_position::SynchronizePlayerPositionPacket; +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::transform::position::Position; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_state::ServerState; +use std::sync::Arc; + +#[derive(NetDecode)] +#[packet(packet_id = "chunk_batch_received", state = "play")] +pub struct ChunkBatchAck { + chunks_per_tick: f32, +} + +impl IncomingPacket for ChunkBatchAck { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + // The first chunk batch should be the ones sent when the player first joins the server. + // This just moves them to their spawn position when all their chunks are done loading, + // preventing them from falling into the floor. + let mut move_to_spawn = false; + { + let mut chunk_recv = state.universe.get_mut::(conn_id)?; + chunk_recv.chunks_per_tick = self.chunks_per_tick; + if !chunk_recv + .has_loaded + .load(std::sync::atomic::Ordering::Relaxed) + { + move_to_spawn = true; + chunk_recv + .has_loaded + .store(true, std::sync::atomic::Ordering::Relaxed); + } + } + { + // If they aren't underground, don't move them to spawn + let pos = state.universe.get_mut::(conn_id)?; + let head_block = state + .world + .get_block_and_fetch(pos.x as i32, pos.y as i32 - 1, pos.z as i32, "overworld") + .await?; + if head_block.name == "minecraft:air" { + move_to_spawn = false; + } + } + if move_to_spawn { + let mut conn = state.universe.get_mut::(conn_id)?; + conn.send_packet( + SynchronizePlayerPositionPacket::default(), + &NetEncodeOpts::WithLength, + )?; + } + Ok(()) + } +} diff --git a/src/lib/net/src/packets/incoming/mod.rs b/src/lib/net/src/packets/incoming/mod.rs index 22de710b..3a9b8063 100644 --- a/src/lib/net/src/packets/incoming/mod.rs +++ b/src/lib/net/src/packets/incoming/mod.rs @@ -11,9 +11,13 @@ pub mod status_request; pub mod keep_alive; pub mod packet_skeleton; +pub mod place_block; +pub mod player_command; pub mod set_player_position; pub mod set_player_position_and_rotation; pub mod set_player_rotation; - -pub mod player_command; pub mod swing_arm; + +pub mod chunk_batch_ack; + +pub mod player_action; diff --git a/src/lib/net/src/packets/incoming/place_block.rs b/src/lib/net/src/packets/incoming/place_block.rs new file mode 100644 index 00000000..9c1649df --- /dev/null +++ b/src/lib/net/src/packets/incoming/place_block.rs @@ -0,0 +1,120 @@ +use crate::connection::StreamWriter; +use crate::packets::outgoing::block_change_ack::BlockChangeAck; +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::collisions::bounds::CollisionBounds; +use ferrumc_core::transform::position::Position; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_net_codec::net_types::network_position::NetworkPosition; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use ferrumc_world::vanilla_chunk_format::BlockData; +use std::sync::Arc; +use tracing::{debug, trace}; + +#[derive(NetDecode, Debug)] +#[packet(packet_id = "use_item_on", state = "play")] +pub struct PlaceBlock { + pub hand: VarInt, + pub position: NetworkPosition, + pub face: VarInt, + pub cursor_x: f32, + pub cursor_y: f32, + pub cursor_z: f32, + pub inside_block: bool, + pub sequence: VarInt, +} + +impl IncomingPacket for PlaceBlock { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + match self.hand.val { + 0 => { + debug!("Placing block at {:?}", self.position); + let block_clicked = state + .clone() + .world + .get_block_and_fetch( + self.position.x, + self.position.y as i32, + self.position.z, + "overworld", + ) + .await?; + trace!("Block clicked: {:?}", block_clicked); + // Use the face to determine the offset of the block to place + let (x_block_offset, y_block_offset, z_block_offset) = match self.face.val { + 0 => (0, -1, 0), + 1 => (0, 1, 0), + 2 => (0, 0, -1), + 3 => (0, 0, 1), + 4 => (-1, 0, 0), + 5 => (1, 0, 0), + _ => (0, 0, 0), + }; + let (x, y, z) = ( + self.position.x + x_block_offset, + self.position.y + y_block_offset, + self.position.z + z_block_offset, + ); + // Check if the block collides with any entities + let does_collide = { + let q = state.universe.query::<(&Position, &CollisionBounds)>(); + q.into_iter().any(|(_, (pos, bounds))| { + bounds.collides( + (pos.x, pos.y, pos.z), + &CollisionBounds { + x_offset_start: 0.0, + x_offset_end: 1.0, + y_offset_start: 0.0, + y_offset_end: 1.0, + z_offset_start: 0.0, + z_offset_end: 1.0, + }, + (x as f64, y as f64, z as f64), + ) + }) + }; + if does_collide { + trace!("Block placement collided with entity"); + return Ok(()); + } + { + if let Ok(mut conn) = state.universe.get_mut::(conn_id) { + let packet = BlockChangeAck { + sequence: self.sequence.clone(), + }; + conn.send_packet(packet, &NetEncodeOpts::WithLength)?; + } else { + debug!("Could not get StreamWriter"); + } + } + let mut chunk = state.world.load_chunk(x >> 4, z >> 4, "overworld").await?; + + chunk.set_block( + x & 0xF, + y as i32, + z & 0xF, + BlockData { + name: "minecraft:stone".to_string(), + properties: None, + }, + )?; + state.world.save_chunk(chunk).await?; + state.world.sync().await?; + let q = state.universe.query::<&mut ChunkReceiver>(); + for (_, mut chunk_recv) in q { + chunk_recv.queue_chunk_resend(x >> 4, z >> 4, "overworld".to_string()); + } + } + 1 => { + trace!("Offhand block placement not implemented"); + } + _ => { + debug!("Invalid hand"); + } + } + Ok(()) + } +} diff --git a/src/lib/net/src/packets/incoming/player_action.rs b/src/lib/net/src/packets/incoming/player_action.rs new file mode 100644 index 00000000..6f063a0b --- /dev/null +++ b/src/lib/net/src/packets/incoming/player_action.rs @@ -0,0 +1,79 @@ +use crate::connection::StreamWriter; +use crate::packets::outgoing::block_change_ack::BlockChangeAck; +use crate::packets::outgoing::chunk_and_light_data::ChunkAndLightData; +use crate::packets::IncomingPacket; +use crate::NetResult; +use ferrumc_macros::{packet, NetDecode}; +use ferrumc_net_codec::encode::NetEncodeOpts; +use ferrumc_net_codec::net_types::network_position::NetworkPosition; +use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::ServerState; +use ferrumc_world::vanilla_chunk_format::BlockData; +use std::sync::Arc; +use tracing::debug; + +#[derive(NetDecode)] +#[packet(packet_id = "player_action", state = "play")] +pub struct PlayerAction { + pub status: VarInt, + pub location: NetworkPosition, + pub face: u8, + pub sequence: VarInt, +} + +impl IncomingPacket for PlayerAction { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol?oldid=2773393#Player_Action + match self.status.val { + 0 => { + let mut chunk = state + .clone() + .world + .load_chunk(self.location.x >> 4, self.location.z >> 4, "overworld") + .await?; + let block = + chunk.get_block(self.location.x, self.location.y as i32, self.location.z)?; + debug!("Block: {:?}", block); + let (relative_x, relative_y, relative_z) = ( + self.location.x & 0xF, + self.location.y as i32, + self.location.z & 0xF, + ); + chunk.set_block(relative_x, relative_y, relative_z, BlockData::default())?; + // debug!(chunk = ?chunk, "Chunk after block placement"); + state.world.save_chunk(chunk.clone()).await?; + state.world.sync().await?; + { + let ack_packet = BlockChangeAck { + sequence: self.sequence, + }; + if let Ok(mut conn) = state.universe.get_mut::(conn_id) { + let chunk_packet = ChunkAndLightData::from_chunk(&chunk)?; + conn.send_packet(chunk_packet, &NetEncodeOpts::WithLength)?; + conn.send_packet(ack_packet, &NetEncodeOpts::WithLength)?; + } else { + debug!( + "Player disconnected before we could send the BlockChangeAck packet" + ); + } + } + // { + // let q = state.universe.query::<&mut ChunkReceiver>(); + // for (_, mut chunk_receiver) in q { + // debug!("Queueing chunk resend"); + // chunk_receiver.queue_chunk_resend( + // self.location.x >> 4, + // self.location.z >> 4, + // "overworld".to_string(), + // ); + // } + // } + } + 1 => { + debug!("You shouldn't be seeing this in creative mode."); + } + _ => {} + }; + Ok(()) + } +} diff --git a/src/lib/net/src/packets/outgoing/block_change_ack.rs b/src/lib/net/src/packets/outgoing/block_change_ack.rs new file mode 100644 index 00000000..8b65934c --- /dev/null +++ b/src/lib/net/src/packets/outgoing/block_change_ack.rs @@ -0,0 +1,9 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::var_int::VarInt; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = "block_changed_ack", state = "play")] +pub struct BlockChangeAck { + pub sequence: VarInt, +} diff --git a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs index ace00168..7965366f 100644 --- a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs +++ b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs @@ -4,10 +4,10 @@ use ferrumc_macros::{packet, NetEncode}; use ferrumc_net_codec::net_types::bitset::BitSet; use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; use ferrumc_net_codec::net_types::var_int::VarInt; -use ferrumc_world::chunk_format::{Chunk, Heightmaps}; +use ferrumc_world::chunk_format::{Chunk, Heightmaps, PaletteType}; use std::io::{Cursor, Write}; use std::ops::Not; -use tracing::{trace, warn}; +use tracing::warn; const SECTIONS: usize = 24; // Number of sections, adjust for your Y range (-64 to 319) @@ -64,7 +64,7 @@ impl ChunkAndLightData { } pub fn from_chunk(chunk: &Chunk) -> Result { - let mut data = Cursor::new(Vec::new()); + let mut raw_data = Cursor::new(Vec::new()); let mut sky_light_data = Vec::new(); let mut block_light_data = Vec::new(); for section in &chunk.sections { @@ -89,49 +89,40 @@ impl ChunkAndLightData { }; block_light_data.push(section_block_light_data); - data.write_u16::(section.block_states.non_air_blocks)?; + raw_data.write_u16::(section.block_states.non_air_blocks)?; - let bits_per_block = section.block_states.bits_per_block; - data.write_u8(bits_per_block)?; - // If bits_per_block is 0, the section is using the single-value palette format - // If bits_per_block is greater than 0, the section is using the indirect palette format - if bits_per_block > 0 { - // Write the palette - VarInt::new(section.block_states.palette.len() as i32).write(&mut data)?; - for palette_entry in §ion.block_states.palette { - palette_entry.write(&mut data)?; + match §ion.block_states.block_data { + PaletteType::Single(val) => { + // debug!("Single palette type: {:?}", (chunk.x, chunk.z)); + raw_data.write_u8(0)?; + val.write(&mut raw_data)?; + VarInt::new(0).write(&mut raw_data)?; } - - // Write the data - VarInt::new(section.block_states.data.len() as i32).write(&mut data)?; - for data_entry in §ion.block_states.data { - data.write_i64::(*data_entry)?; - } - } else { - // The 0s for air blocks and bits_per_block are already written - // Get the only palette entry - match section.block_states.palette.first() { - Some(palette_entry) => { - palette_entry.write(&mut data)?; + PaletteType::Indirect { + bits_per_block, + data, + palette, + } => { + // debug!("Indirect palette type: {:?}", (chunk.x, chunk.z)); + raw_data.write_u8(*bits_per_block)?; + VarInt::new(palette.len() as i32).write(&mut raw_data)?; + for palette_entry in palette { + palette_entry.write(&mut raw_data)?; } - // If there is no palette entry, write a 0 (air) and log a warning - None => { - VarInt::new(0).write(&mut data)?; - trace!( - "No palette entry found for section at {}, {}, {}", - chunk.x, - section.y, - chunk.z - ); + VarInt::new(data.len() as i32).write(&mut raw_data)?; + for data_entry in data { + raw_data.write_i64::(*data_entry)?; } } - // Write the empty data section's length (0) - VarInt::new(0).write(&mut data)?; + PaletteType::Direct { .. } => { + todo!("Direct palette type") + } } + // Empty biome data for now - data.write_u8(0)?; - data.write_u8(0)?; - data.write_u8(0)?; + raw_data.write_u8(0)?; + raw_data.write_u8(0)?; + raw_data.write_u8(0)?; } let mut sky_light_mask = BitSet::new(SECTIONS + 2); let mut block_light_mask = BitSet::new(SECTIONS + 2); @@ -168,7 +159,7 @@ impl ChunkAndLightData { chunk_x: chunk.x, chunk_z: chunk.z, heightmaps: chunk.heightmaps.serialize_as_network(), - data: LengthPrefixedVec::new(data.into_inner()), + data: LengthPrefixedVec::new(raw_data.into_inner()), block_entities: LengthPrefixedVec::new(Vec::new()), sky_light_mask, block_light_mask, diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index ce630555..d14a7b6e 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -32,3 +32,5 @@ pub mod update_entity_position; pub mod update_entity_position_and_rotation; pub mod update_entity_rotation; // ----------------------------- + +pub mod block_change_ack; diff --git a/src/lib/net/src/utils/broadcast.rs b/src/lib/net/src/utils/broadcast.rs index fcbcc266..c5ec9e21 100644 --- a/src/lib/net/src/utils/broadcast.rs +++ b/src/lib/net/src/utils/broadcast.rs @@ -1,7 +1,7 @@ use crate::connection::StreamWriter; use crate::NetResult; use async_trait::async_trait; -use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_ecs::entities::Entity; use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts}; use ferrumc_state::GlobalState; @@ -71,7 +71,7 @@ pub fn get_all_play_players(state: &GlobalState) -> HashSet { state .universe .get_component_manager() - .get_entities_with::() + .get_entities_with::() .into_iter() .collect() } diff --git a/src/lib/storage/src/lmdb.rs b/src/lib/storage/src/lmdb.rs index fe5fd660..4549b8bb 100644 --- a/src/lib/storage/src/lmdb.rs +++ b/src/lib/storage/src/lmdb.rs @@ -18,7 +18,6 @@ impl From for StorageError { Error::Io(e) => StorageError::GenericIoError(e), Error::Encoding(e) => StorageError::WriteError(e.to_string()), Error::Decoding(e) => StorageError::ReadError(e.to_string()), - Error::DatabaseClosing => StorageError::CloseError("Database closing".to_string()), _ => StorageError::DatabaseError(err.to_string()), } } @@ -45,7 +44,7 @@ impl LmdbBackend { let rounded_map_size = ((map_size as f64 / page_size::get() as f64).round() * page_size::get() as f64) as usize; unsafe { - Ok(LmdbBackend { + let backend = LmdbBackend { env: Arc::new( EnvOpenOptions::new() // Change this as more tables are needed. @@ -54,7 +53,8 @@ impl LmdbBackend { .open(checked_path) .map_err(|e| StorageError::DatabaseInitError(e.to_string()))?, ), - }) + }; + Ok(backend) } } @@ -211,6 +211,17 @@ impl LmdbBackend { .expect("Failed to run tokio task") } + pub async fn table_exists(&self, table: String) -> Result { + let env = self.env.clone(); + tokio::task::spawn_blocking(move || { + let ro_txn = env.read_txn()?; + let db = env.open_database::, Bytes>(&ro_txn, Some(&table))?; + Ok(db.is_some()) + }) + .await + .expect("Failed to run tokio task") + } + pub async fn details(&self) -> String { format!("LMDB (heed 0.20.5): {:?}", self.env.info()) } diff --git a/src/lib/utils/config/src/server_config.rs b/src/lib/utils/config/src/server_config.rs index 127e2433..bc1392b7 100644 --- a/src/lib/utils/config/src/server_config.rs +++ b/src/lib/utils/config/src/server_config.rs @@ -34,14 +34,14 @@ pub struct ServerConfig { /// Fields: /// - `cache_size`: The cache size in KB. /// - `compression` - Which compression algorithm to use. Options are `brotli`, `deflate`, `gzip`, `zlib` -/// and `zstd` +/// and `zstd` /// - `world_path`: The path to the world database. /// - `compression_level`: The compression level to use. This is a number from 0-22. Not all compressors -/// support levels, so this will be a no-op for some compressors. +/// support levels, so this will be a no-op for some compressors. /// - `map_size`: The max size of the database's memory map. Basically you need this to be big enough -/// to hold everything before it starts writing to disk. This isn't memory use though, it's just -/// how much we can map into memory if needed, so you can set this to an insane number if you want, -/// but it won't actually use that much memory, it'll just show up as virtual memory use. +/// to hold everything before it starts writing to disk. This isn't memory use though, it's just +/// how much we can map into memory if needed, so you can set this to an insane number if you want, +/// but it won't actually use that much memory, it'll just show up as virtual memory use. /// - `cache_ttl`: The time to live for cache entries in seconds. /// - `cache_capacity`: How big the cache can be in kb. #[derive(Debug, Deserialize, Serialize)] diff --git a/src/lib/utils/config/src/whitelist.rs b/src/lib/utils/config/src/whitelist.rs index ffc268ad..f73b28eb 100644 --- a/src/lib/utils/config/src/whitelist.rs +++ b/src/lib/utils/config/src/whitelist.rs @@ -186,10 +186,7 @@ async fn query_mojang_for_usernames(uuids: Vec<&Uuid>) -> Vec { match response { Ok(response) if response.status().is_success() => { - match response.json::().await { - Ok(parsed_response) => Some(parsed_response), - Err(_) => None, - } + response.json::().await.ok() } _ => None, } diff --git a/src/lib/utils/general_purpose/src/data_packing/errors.rs b/src/lib/utils/general_purpose/src/data_packing/errors.rs new file mode 100644 index 00000000..0ea7f02a --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/errors.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DataPackingError { + #[error("Size ({0}) exceeds maximum size of data type: {1}")] + SizeExceedsMaxSize(u8, u8), + #[error("Not enough bits to read with size {0} at offset {1}")] + NotEnoughBits(u8, u32), +} diff --git a/src/lib/utils/general_purpose/src/data_packing/i16.rs b/src/lib/utils/general_purpose/src/data_packing/i16.rs new file mode 100644 index 00000000..e63e62c0 --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/i16.rs @@ -0,0 +1,101 @@ +use crate::data_packing::errors::DataPackingError; + +/// Reads a specified number of bits from a given offset in a 64-bit signed integer. +/// +/// # Arguments +/// +/// * `data` - A reference to the 64-bit signed integer to read from. +/// * `size` - The number of bits to read (must be 16 or less). +/// * `offset` - The bit offset from which to start reading. +/// +/// # Returns +/// +/// * `Ok(i16)` - The extracted bits as a 16-bit signed integer. +/// * `Err(DataPackingError)` - If the size exceeds 16 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 16. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn read_nbit_i16(data: &i64, size: u8, offset: u32) -> Result { + if size > 16 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 16)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = (1 << size) - 1; + let extracted_bits = ((data >> offset) & mask) as i16; + // Sign extend if the extracted bits represent a negative number + let sign_bit = 1 << (size - 1); + if extracted_bits & sign_bit != 0 { + Ok(extracted_bits | !mask as i16) + } else { + Ok(extracted_bits) + } +} + +/// Writes a specified number of bits to a given offset in a 64-bit signed integer. +/// +/// # Arguments +/// +/// * `data` - A mutable reference to the 64-bit signed integer to write to. +/// * `offset` - The bit offset from which to start writing. +/// * `value` - The 16-bit signed integer value to write. +/// * `size` - The number of bits to write (must be 16 or less). +/// +/// # Returns +/// +/// * `Ok(())` - If the bits were successfully written. +/// * `Err(DataPackingError)` - If the size exceeds 16 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 16. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn write_nbit_i16( + data: &mut i64, + offset: u32, + value: i16, + size: u8, +) -> Result<(), DataPackingError> { + if size > 16 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 16)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = (1 << size) - 1; + *data &= !(mask << offset); + *data |= ((value as i64) & mask) << offset; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests the `read_nbit_i16` function with various inputs. + #[test] + fn test_read_nbit_i16() { + let data: i64 = 0b110101011; + assert_eq!(read_nbit_i16(&data, 3, 0).unwrap(), 0b011); + assert_eq!(read_nbit_i16(&data, 3, 3).unwrap(), -3); // 0b101 as i16 is -3 + assert_eq!(read_nbit_i16(&data, 3, 6).unwrap(), -2); // 0b110 as i16 is -2 + assert_eq!(read_nbit_i16(&data, 3, 9).unwrap(), 0b000); + } + + /// Tests the `write_nbit_i16` function with various inputs. + #[test] + fn test_write_nbit_i16() { + let mut data: i64 = 0; + write_nbit_i16(&mut data, 0, 0b011, 3).unwrap(); + assert_eq!(data, 0b011); + write_nbit_i16(&mut data, 3, -3, 3).unwrap(); // 0b101 as i16 is -3 + assert_eq!(data, 0b101011); + write_nbit_i16(&mut data, 6, -2, 3).unwrap(); // 0b110 as i16 is -2 + assert_eq!(data, 0b110101011); + write_nbit_i16(&mut data, 9, 0b000, 3).unwrap(); + assert_eq!(data, 0b110101011); + } +} diff --git a/src/lib/utils/general_purpose/src/data_packing/i32.rs b/src/lib/utils/general_purpose/src/data_packing/i32.rs new file mode 100644 index 00000000..41ec07da --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/i32.rs @@ -0,0 +1,109 @@ +use crate::data_packing::errors::DataPackingError; + +/// Reads a specified number of bits from a given offset in a 64-bit signed integer. +/// +/// # Arguments +/// +/// * `data` - A reference to the 64-bit signed integer to read from. +/// * `size` - The number of bits to read (must be 32 or less). +/// * `offset` - The bit offset from which to start reading. +/// +/// # Returns +/// +/// * `Ok(i32)` - The extracted bits as a 32-bit signed integer. +/// * `Err(DataPackingError)` - If the size exceeds 32 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 32. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +/// Reads an n-bit integer from a packed `i64`. +pub fn read_nbit_i32( + word: &i64, + bit_size: usize, + bit_offset: u32, +) -> Result { + if bit_size == 0 || bit_size > 32 { + return Err(DataPackingError::SizeExceedsMaxSize(bit_size as u8, 32)); + } + if bit_offset >= 64 { + return Err(DataPackingError::SizeExceedsMaxSize(bit_size as u8, 32)); + } + if bit_offset + bit_size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(bit_size as u8, bit_offset)); + } + + // Create a mask for the n-bit value + let mask = (1u64 << bit_size) - 1; + + // Extract the value from the word + let value = ((*word as u64) >> bit_offset) & mask; + + // Cast to i32 and return + Ok(value as i32) +} + +/// Writes a specified number of bits to a given offset in a 64-bit signed integer. +/// +/// # Arguments +/// +/// * `data` - A mutable reference to the 64-bit signed integer to write to. +/// * `offset` - The bit offset from which to start writing. +/// * `value` - The 32-bit signed integer value to write. +/// * `size` - The number of bits to write (must be 32 or less). +/// +/// # Returns +/// +/// * `Ok(())` - If the bits were successfully written. +/// * `Err(DataPackingError)` - If the size exceeds 32 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 32. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn write_nbit_i32( + data: &mut i64, + offset: u32, + value: i32, + size: u8, +) -> Result<(), DataPackingError> { + if size > 32 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 32)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = (1 << size) - 1; + *data &= !(mask << offset); + *data |= ((value as i64) & mask) << offset; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests the `read_nbit_i32` function with various inputs. + #[test] + fn test_read_nbit_i32() { + let data: i64 = 0b110101011; + assert_eq!(read_nbit_i32(&data, 3, 0).unwrap(), 0b011); + assert_eq!(read_nbit_i32(&data, 3, 3).unwrap(), 0b101); + assert_eq!(read_nbit_i32(&data, 3, 6).unwrap(), 0b110); + assert_eq!(read_nbit_i32(&data, 3, 9).unwrap(), 0b000); + } + + /// Tests the `write_nbit_i32` function with various inputs. + #[test] + fn test_write_nbit_i32() { + let mut data: i64 = 0; + write_nbit_i32(&mut data, 0, 0b011, 3).unwrap(); + assert_eq!(data, 0b011); + write_nbit_i32(&mut data, 3, -3, 3).unwrap(); // 0b101 as i32 is -3 + assert_eq!(data, 0b101011); + write_nbit_i32(&mut data, 6, -2, 3).unwrap(); // 0b110 as i32 is -2 + assert_eq!(data, 0b110101011); + write_nbit_i32(&mut data, 9, 0b000, 3).unwrap(); + assert_eq!(data, 0b110101011); + } +} diff --git a/src/lib/utils/general_purpose/src/data_packing/i8.rs b/src/lib/utils/general_purpose/src/data_packing/i8.rs new file mode 100644 index 00000000..310d72e5 --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/i8.rs @@ -0,0 +1,101 @@ +use crate::data_packing::errors::DataPackingError; + +/// Reads a specified number of bits from a given offset in a 64-bit signed integer. +/// +/// # Arguments +/// +/// * `data` - A reference to the 64-bit signed integer to read from. +/// * `size` - The number of bits to read (must be 8 or less). +/// * `offset` - The bit offset from which to start reading. +/// +/// # Returns +/// +/// * `Ok(i8)` - The extracted bits as an 8-bit signed integer. +/// * `Err(DataPackingError)` - If the size exceeds 8 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 8. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn read_nbit_i8(data: &i64, size: u8, offset: u32) -> Result { + if size > 8 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 8)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = (1 << size) - 1; + let extracted_bits = ((data >> offset) & mask) as i8; + // Sign extend if the extracted bits represent a negative number + let sign_bit = 1 << (size - 1); + if extracted_bits & sign_bit != 0 { + Ok(extracted_bits | !mask as i8) + } else { + Ok(extracted_bits) + } +} + +/// Writes a specified number of bits to a given offset in a 64-bit signed integer. +/// +/// # Arguments +/// +/// * `data` - A mutable reference to the 64-bit signed integer to write to. +/// * `offset` - The bit offset from which to start writing. +/// * `value` - The 8-bit signed integer value to write. +/// * `size` - The number of bits to write (must be 8 or less). +/// +/// # Returns +/// +/// * `Ok(())` - If the bits were successfully written. +/// * `Err(DataPackingError)` - If the size exceeds 8 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 8. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn write_nbit_i8( + data: &mut i64, + offset: u32, + value: i8, + size: u8, +) -> Result<(), DataPackingError> { + if size > 8 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 8)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = (1 << size) - 1; + *data &= !((mask) << offset); + *data |= ((value as i64) & mask) << offset; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests the `write_nbit_i8` function with various inputs. + #[test] + fn test_write_nbit_i8() { + let mut data: i64 = 0; + write_nbit_i8(&mut data, 0, 0b011, 3).unwrap(); + assert_eq!(data, 0b011); + write_nbit_i8(&mut data, 3, -3, 3).unwrap(); // 0b101 as i8 is -3 + assert_eq!(data, 0b101011); + write_nbit_i8(&mut data, 6, -2, 3).unwrap(); // 0b110 as i8 is -2 + assert_eq!(data, 0b110101011); + write_nbit_i8(&mut data, 9, 0b000, 3).unwrap(); + assert_eq!(data, 0b110101011); + } + + /// Tests the `read_nbit_i8` function with various inputs. + #[test] + fn test_read_nbit_i8() { + let data: i64 = 0b110101011; + assert_eq!(read_nbit_i8(&data, 3, 0).unwrap(), 0b011); + assert_eq!(read_nbit_i8(&data, 3, 3).unwrap(), -3); // 0b101 as i8 is -3 + assert_eq!(read_nbit_i8(&data, 3, 6).unwrap(), -2); // 0b110 as i8 is -2 + assert_eq!(read_nbit_i8(&data, 3, 9).unwrap(), 0b000); + } +} diff --git a/src/lib/utils/general_purpose/src/data_packing/mod.rs b/src/lib/utils/general_purpose/src/data_packing/mod.rs new file mode 100644 index 00000000..4afa9f63 --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/mod.rs @@ -0,0 +1,7 @@ +pub mod errors; +pub mod i16; +pub mod i32; +pub mod i8; +pub mod u16; +pub mod u32; +pub mod u8; diff --git a/src/lib/utils/general_purpose/src/data_packing/u16.rs b/src/lib/utils/general_purpose/src/data_packing/u16.rs new file mode 100644 index 00000000..e697bc8b --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/u16.rs @@ -0,0 +1,93 @@ +use crate::data_packing::errors::DataPackingError; + +/// Reads a specified number of bits from a given offset in a 64-bit unsigned integer. +/// +/// # Arguments +/// +/// * `data` - A reference to the 64-bit unsigned integer to read from. +/// * `size` - The number of bits to read (must be 16 or less). +/// * `offset` - The bit offset from which to start reading. +/// +/// # Returns +/// +/// * `Ok(u16)` - The extracted bits as a 16-bit unsigned integer. +/// * `Err(DataPackingError)` - If the size exceeds 16 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 16. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn read_nbit_u16(data: &u64, size: u8, offset: u32) -> Result { + if size > 16 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 16)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + Ok(((data >> offset) & ((1 << size) - 1)) as u16) +} + +/// Writes a specified number of bits to a given offset in a 64-bit unsigned integer. +/// +/// # Arguments +/// +/// * `data` - A mutable reference to the 64-bit unsigned integer to write to. +/// * `offset` - The bit offset from which to start writing. +/// * `value` - The 16-bit unsigned integer value to write. +/// * `size` - The number of bits to write (must be 16 or less). +/// +/// # Returns +/// +/// * `Ok(())` - If the bits were successfully written. +/// * `Err(DataPackingError)` - If the size exceeds 16 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 16. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn write_nbit_u16( + data: &mut u64, + offset: u32, + value: u16, + size: u8, +) -> Result<(), DataPackingError> { + if size > 16 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 16)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = (1 << size) - 1; + *data &= !((mask) << offset); + *data |= ((value as u64) & mask) << offset; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests the `read_nbit_u16` function with various inputs. + #[test] + fn test_read_nbit_u16() { + let data: u64 = 0b110101011; + assert_eq!(read_nbit_u16(&data, 3, 0).unwrap(), 0b011); + assert_eq!(read_nbit_u16(&data, 3, 3).unwrap(), 0b101); + assert_eq!(read_nbit_u16(&data, 3, 6).unwrap(), 0b110); + assert_eq!(read_nbit_u16(&data, 3, 9).unwrap(), 0b000); + } + + /// Tests the `write_nbit_u16` function with various inputs. + #[test] + fn test_write_nbit_u16() { + let mut data: u64 = 0; + write_nbit_u16(&mut data, 0, 0b011, 3).unwrap(); + assert_eq!(data, 0b011); + write_nbit_u16(&mut data, 3, 0b101, 3).unwrap(); + assert_eq!(data, 0b101011); + write_nbit_u16(&mut data, 6, 0b110, 3).unwrap(); + assert_eq!(data, 0b110101011); + write_nbit_u16(&mut data, 9, 0b000, 3).unwrap(); + assert_eq!(data, 0b110101011); + } +} diff --git a/src/lib/utils/general_purpose/src/data_packing/u32.rs b/src/lib/utils/general_purpose/src/data_packing/u32.rs new file mode 100644 index 00000000..92175e21 --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/u32.rs @@ -0,0 +1,96 @@ +use crate::data_packing::errors::DataPackingError; + +/// Reads a specified number of bits from a given offset in a 64-bit unsigned integer. +/// +/// # Arguments +/// +/// * `data` - A reference to the 64-bit unsigned integer to read from. +/// * `size` - The number of bits to read (must be 32 or less). +/// * `offset` - The bit offset from which to start reading. +/// +/// # Returns +/// +/// * `Ok(u32)` - The extracted bits as a 32-bit unsigned integer. +/// * `Err(DataPackingError)` - If the size exceeds 32 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 32. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn read_nbit_u32(data: &i64, size: u8, offset: u32) -> Result { + if size > 32 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 32)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + Ok(((*data as u64 >> offset as u64) & ((1u64 << size) - 1u64)) as u32) +} + +/// Writes a specified number of bits to a given offset in a 64-bit unsigned integer. +/// +/// # Arguments +/// +/// * `data` - A mutable reference to the 64-bit unsigned integer to write to. +/// * `offset` - The bit offset from which to start writing. +/// * `value` - The 32-bit unsigned integer value to write. +/// * `size` - The number of bits to write (must be 32 or less). +/// +/// # Returns +/// +/// * `Ok(())` - If the bits were successfully written. +/// * `Err(DataPackingError)` - If the size exceeds 32 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 32. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn write_nbit_u32( + data: &mut i64, + offset: u32, + value: u32, + size: u8, +) -> Result<(), DataPackingError> { + if size == 0 { + return Ok(()); // Nothing to do for 0 bits + } + if size > 32 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 32)); + } + if offset >= 64 || offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + let mask = ((1u64 << size) - 1) as i64; // Use u64 to avoid overflow + *data &= !(mask << offset); // Clear the target bits + *data |= ((value as i64) & mask) << offset; // Write the new value + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests the `read_nbit_u32` function with various inputs. + #[test] + fn test_read_nbit_u32() { + let data: i64 = 0b110101011; + assert_eq!(read_nbit_u32(&data, 3, 0).unwrap(), 0b011); + assert_eq!(read_nbit_u32(&data, 3, 3).unwrap(), 0b101); + assert_eq!(read_nbit_u32(&data, 3, 6).unwrap(), 0b110); + assert_eq!(read_nbit_u32(&data, 3, 9).unwrap(), 0b000); + } + + /// Tests the `write_nbit_u32` function with various inputs. + #[test] + fn test_write_nbit_u32() { + let mut data: i64 = 0; + write_nbit_u32(&mut data, 0, 0b011, 3).unwrap(); + assert_eq!(data, 0b011); + write_nbit_u32(&mut data, 3, 0b101, 3).unwrap(); + assert_eq!(data, 0b101011); + write_nbit_u32(&mut data, 6, 0b110, 3).unwrap(); + assert_eq!(data, 0b110101011); + write_nbit_u32(&mut data, 9, 0b000, 3).unwrap(); + assert_eq!(data, 0b110101011); + } +} diff --git a/src/lib/utils/general_purpose/src/data_packing/u8.rs b/src/lib/utils/general_purpose/src/data_packing/u8.rs new file mode 100644 index 00000000..a05b04ae --- /dev/null +++ b/src/lib/utils/general_purpose/src/data_packing/u8.rs @@ -0,0 +1,92 @@ +use crate::data_packing::errors::DataPackingError; + +/// Reads a specified number of bits from a given offset in a 64-bit unsigned integer. +/// +/// # Arguments +/// +/// * `data` - A reference to the 64-bit unsigned integer to read from. +/// * `size` - The number of bits to read (must be 8 or less). +/// * `offset` - The bit offset from which to start reading. +/// +/// # Returns +/// +/// * `Ok(u8)` - The extracted bits as an 8-bit unsigned integer. +/// * `Err(DataPackingError)` - If the size exceeds 8 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 8. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn read_nbit_u8(data: &i64, size: u8, offset: u32) -> Result { + if size > 8 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 8)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + Ok(((data >> offset) & ((1 << size) - 1)) as u8) +} + +/// Writes a specified number of bits to a given offset in a 64-bit unsigned integer. +/// +/// # Arguments +/// +/// * `data` - A mutable reference to the 64-bit unsigned integer to write to. +/// * `offset` - The bit offset from which to start writing. +/// * `value` - The 8-bit unsigned integer value to write. +/// * `size` - The number of bits to write (must be 8 or less). +/// +/// # Returns +/// +/// * `Ok(())` - If the bits were successfully written. +/// * `Err(DataPackingError)` - If the size exceeds 8 bits or the offset plus size exceeds 64 bits. +/// +/// # Errors +/// +/// * `DataPackingError::SizeExceedsMaxSize` - If `size` is greater than 8. +/// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. +pub fn write_nbit_u8( + data: &mut u64, + offset: u32, + value: u8, + size: u8, +) -> Result<(), DataPackingError> { + if size > 8 { + return Err(DataPackingError::SizeExceedsMaxSize(size, 8)); + } + if offset + size as u32 > 64 { + return Err(DataPackingError::NotEnoughBits(size, offset)); + } + *data &= !(((1 << size) - 1) << offset); + *data |= (value as u64) << offset; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests the `read_nbit_u8` function with various inputs. + #[test] + fn test_read_nbit_u8() { + let data: i64 = 0b110101011; + assert_eq!(read_nbit_u8(&data, 3, 0).unwrap(), 0b011); + assert_eq!(read_nbit_u8(&data, 3, 3).unwrap(), 0b101); + assert_eq!(read_nbit_u8(&data, 3, 6).unwrap(), 0b110); + assert_eq!(read_nbit_u8(&data, 3, 9).unwrap(), 0b000); + } + + /// Tests the `write_nbit_u8` function with various inputs. + #[test] + fn test_write_nbit_u8() { + let mut data: u64 = 0; + write_nbit_u8(&mut data, 0, 0b011, 3).unwrap(); + assert_eq!(data, 0b011); + write_nbit_u8(&mut data, 3, 0b101, 3).unwrap(); + assert_eq!(data, 0b101011); + write_nbit_u8(&mut data, 6, 0b110, 3).unwrap(); + assert_eq!(data, 0b110101011); + write_nbit_u8(&mut data, 9, 0b000, 3).unwrap(); + assert_eq!(data, 0b110101011); + } +} diff --git a/src/lib/utils/general_purpose/src/lib.rs b/src/lib/utils/general_purpose/src/lib.rs index 3dc1a972..d6eb177f 100644 --- a/src/lib/utils/general_purpose/src/lib.rs +++ b/src/lib/utils/general_purpose/src/lib.rs @@ -1,3 +1,4 @@ +pub mod data_packing; pub mod hashing; pub mod paths; pub mod simd; diff --git a/src/lib/utils/logging/src/lib.rs b/src/lib/utils/logging/src/lib.rs index 97fece40..fe9ebbed 100644 --- a/src/lib/utils/logging/src/lib.rs +++ b/src/lib/utils/logging/src/lib.rs @@ -45,6 +45,10 @@ pub fn init_logging(trace_level: Level) { .with_thread_ids(false) .with_thread_names(false); } + #[cfg(debug_assertions)] + { + fmt_layer = fmt_layer.with_file(true).with_line_number(true); + } let profiler_layer = ProfilerTracingLayer; diff --git a/src/lib/world/Cargo.toml b/src/lib/world/Cargo.toml index 2ccc3a3f..03b0089e 100644 --- a/src/lib/world/Cargo.toml +++ b/src/lib/world/Cargo.toml @@ -6,9 +6,6 @@ edition = "2021" [dependencies] thiserror = { workspace = true } - -ferrumc-logging = { workspace = true } -ferrumc-profiling = { workspace = true } ferrumc-storage = { workspace = true } ferrumc-config = { workspace = true } tracing = { workspace = true } @@ -31,4 +28,3 @@ serde_json = { workspace = true } indicatif = { workspace = true } wyhash = { workspace = true } moka = { workspace = true, features = ["future"] } -log = "0.4.22" diff --git a/src/lib/world/src/chunk_format.rs b/src/lib/world/src/chunk_format.rs index 49d9fd26..0b150ddb 100644 --- a/src/lib/world/src/chunk_format.rs +++ b/src/lib/world/src/chunk_format.rs @@ -1,15 +1,19 @@ use crate::errors::WorldError; +use crate::errors::WorldError::InvalidBlockStateData; use crate::vanilla_chunk_format; use crate::vanilla_chunk_format::VanillaChunk; use bitcode_derive::{Decode, Encode}; use deepsize::DeepSizeOf; +use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; use ferrumc_macros::{NBTDeserialize, NBTSerialize}; use ferrumc_net_codec::net_types::var_int::VarInt; use lazy_static::lazy_static; +use std::cmp::max; +use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io::Read; -use tracing::error; -use vanilla_chunk_format::Palette; +use tracing::{error, warn}; +use vanilla_chunk_format::BlockData; #[cfg(test)] const BLOCKSFILE: &[u8] = &[0]; @@ -23,21 +27,21 @@ const BLOCKSFILE: &[u8] = &[0]; const BLOCKSFILE: &[u8] = include_bytes!("../../../../.etc/blockmappings.bz2"); lazy_static! { - static ref ID2BLOCK: HashMap = { + pub static ref ID2BLOCK: HashMap = { let mut bzipreader = bzip2::read::BzDecoder::new(BLOCKSFILE); let mut output = String::new(); bzipreader.read_to_string(&mut output).unwrap(); - let string_keys: HashMap = serde_json::from_str(&output).unwrap(); + let string_keys: HashMap = serde_json::from_str(&output).unwrap(); string_keys .iter() .map(|(k, v)| (k.parse::().unwrap(), v.clone())) .collect() }; - static ref BLOCK2ID: HashMap = + pub static ref BLOCK2ID: HashMap = ID2BLOCK.iter().map(|(k, v)| (v.clone(), *k)).collect(); } -#[derive(Encode, Decode, Clone, DeepSizeOf)] +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] // This is a placeholder for the actual chunk format pub struct Chunk { pub x: i32, @@ -47,32 +51,52 @@ pub struct Chunk { pub heightmaps: Heightmaps, } -#[derive(Encode, Decode, NBTDeserialize, NBTSerialize, Clone, DeepSizeOf)] +#[derive(Encode, Decode, NBTDeserialize, NBTSerialize, Clone, DeepSizeOf, Debug)] #[nbt(net_encode)] +#[derive(Eq, PartialEq)] pub struct Heightmaps { #[nbt(rename = "MOTION_BLOCKING")] pub motion_blocking: Vec, #[nbt(rename = "WORLD_SURFACE")] pub world_surface: Vec, } -#[derive(Encode, Decode, Clone, DeepSizeOf)] +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] pub struct Section { pub y: i8, pub block_states: BlockStates, - pub biome_data: Vec, - pub biome_palette: Vec, + pub biome_states: BiomeStates, pub block_light: Vec, pub sky_light: Vec, } -#[derive(Encode, Decode, Clone, DeepSizeOf)] +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] pub struct BlockStates { - pub bits_per_block: u8, pub non_air_blocks: u16, + pub block_data: PaletteType, + pub block_counts: HashMap, +} + +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] +pub enum PaletteType { + Single(VarInt), + Indirect { + bits_per_block: u8, + data: Vec, + palette: Vec, + }, + Direct { + bits_per_block: u8, + data: Vec, + }, +} + +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] +pub struct BiomeStates { + pub bits_per_biome: u8, pub data: Vec, pub palette: Vec, } -fn convert_to_net_palette(vanilla_palettes: Vec) -> Result, WorldError> { +fn convert_to_net_palette(vanilla_palettes: Vec) -> Result, WorldError> { let mut new_palette = Vec::new(); for palette in vanilla_palettes { if let Some(id) = BLOCK2ID.get(&palette) { @@ -105,7 +129,8 @@ impl VanillaChunk { let mut sections = Vec::new(); for section in self.sections.as_ref().unwrap() { let y = section.y; - let block_data = section + let block_data: PaletteType; + let raw_block_data = section .block_states .as_ref() .and_then(|bs| bs.data.clone()) @@ -115,21 +140,53 @@ impl VanillaChunk { .as_ref() .and_then(|bs| bs.palette.clone()) .unwrap_or_default(); - let biome_data = section - .biomes - .as_ref() - .and_then(|biome_data| biome_data.data.clone()) - .unwrap_or_default(); - let biome_palette = section - .biomes - .as_ref() - .map_or(vec![], |biome_data| biome_data.palette.clone()); - let non_air_blocks = palette.iter().filter(|id| id.name != "air").count() as u16; + let bits_per_block = max((palette.len() as f32).log2().ceil() as u8, 4); + let mut block_counts = HashMap::new(); + for chunk in &raw_block_data { + let mut i = 0; + while i + bits_per_block < 64 { + let palette_index = read_nbit_i32(chunk, bits_per_block as usize, i as u32)?; + let block = match palette.get(palette_index as usize) { + Some(block) => block, + None => { + error!("Could not find block for palette index: {}", palette_index); + &BlockData::default() + } + }; + *block_counts.entry(block.clone()).or_insert(0) += 1; + i += bits_per_block; + } + } + if raw_block_data.is_empty() { + let single_block = BlockData::default(); + block_data = PaletteType::Single(VarInt::from(0)); + block_counts.insert(single_block.clone(), 4096); + } else { + block_data = PaletteType::Indirect { + bits_per_block, + data: raw_block_data, + palette: convert_to_net_palette(palette)?, + }; + } + // Count the number of blocks that are either air, void air, or cave air + let mut air_blocks = *block_counts.get(&BlockData::default()).unwrap_or(&0) as u16; + air_blocks += *block_counts + .get(&BlockData { + name: "minecraft:void_air".to_string(), + properties: None, + }) + .unwrap_or(&0) as u16; + air_blocks += *block_counts + .get(&BlockData { + name: "minecraft:cave_air".to_string(), + properties: None, + }) + .unwrap_or(&0) as u16; + let non_air_blocks = 4096 - air_blocks; let block_states = BlockStates { - bits_per_block: (palette.len() as f32).log2().ceil() as u8, + block_counts, non_air_blocks, - data: block_data, - palette: convert_to_net_palette(palette)?, + block_data, }; let block_light = section .block_light @@ -145,11 +202,16 @@ impl VanillaChunk { .iter() .map(|x| *x as u8) .collect(); + let biome_states = BiomeStates { + // TODO: Implement biome states properly + bits_per_biome: 4, + data: vec![], + palette: vec![VarInt::from(0); 1], + }; let section = Section { y, block_states, - biome_data, - biome_palette, + biome_states, block_light, sky_light, }; @@ -177,3 +239,493 @@ impl VanillaChunk { }) } } + +impl BlockStates { + pub fn resize(&mut self, new_bit_size: usize) -> Result<(), WorldError> { + match &mut self.block_data { + PaletteType::Single(val) => { + let block = ID2BLOCK + .get(&val.val) + .cloned() + .unwrap_or(BlockData::default()); + let mut new_palette = vec![VarInt::from(0); 1]; + if let Some(id) = BLOCK2ID.get(&block) { + new_palette[0] = VarInt::from(*id); + } else { + error!("Could not find block id for palette entry: {:?}", block); + } + self.block_data = PaletteType::Indirect { + bits_per_block: new_bit_size as u8, + data: vec![], + palette: new_palette, + } + } + PaletteType::Indirect { + bits_per_block, + data, + palette, + } => { + // Step 1: Read existing packed data into a list of normal integers + let mut normalised_ints = Vec::with_capacity(4096); + let mut values_read = 0; + + for long in data { + let mut bit_offset = 0; + + while bit_offset + *bits_per_block as usize <= 64 { + if values_read >= 4096 { + break; + } + + // Extract value at the current bit offset + let value = + read_nbit_i32(long, *bits_per_block as usize, bit_offset as u32)?; + let max_int_value = (1 << new_bit_size) - 1; + if value > max_int_value { + return Err(InvalidBlockStateData(format!( + "Value {} exceeds maximum value for {}-bit block state", + value, new_bit_size + ))); + } + normalised_ints.push(value); + values_read += 1; + + bit_offset += *bits_per_block as usize; + } + + // Stop reading if we’ve already hit 4096 values + if values_read >= 4096 { + break; + } + } + + // Check if we read exactly 4096 block states + if normalised_ints.len() != 4096 { + return Err(InvalidBlockStateData(format!( + "Expected 4096 block states, but got {}", + normalised_ints.len() + ))); + } + + // Step 2: Write the normalised integers into the new packed format + let mut new_data = Vec::new(); + let mut current_long: i64 = 0; + let mut bit_position = 0; + + for &value in &normalised_ints { + current_long |= (value as i64) << bit_position; + bit_position += new_bit_size; + + if bit_position >= 64 { + new_data.push(current_long); + current_long = (value as i64) >> (new_bit_size - (bit_position - 64)); + bit_position -= 64; + } + } + + // Push any remaining bits in the final long + if bit_position > 0 { + new_data.push(current_long); + } + + // Verify the size of the new data matches expectations + let expected_size = (4096 * new_bit_size).div_ceil(64); + if new_data.len() != expected_size { + return Err(InvalidBlockStateData(format!( + "Expected packed data size of {}, but got {}", + expected_size, + new_data.len() + ))); + } + // Update the chunk with the new packed data and bit size + self.block_data = PaletteType::Indirect { + bits_per_block: new_bit_size as u8, + data: new_data, + palette: palette.clone(), + } + } + _ => { + todo!("Implement resizing for direct palette") + } + }; + Ok(()) + } +} + +impl Chunk { + /// Sets the block at the specified coordinates to the specified block data. + /// If the block is the same as the old block, nothing happens. + /// If the block is not in the palette, it is added. + /// If the palette is in single block mode, it is converted to palette'd mode. + /// + /// # Arguments + /// + /// * `x` - The x-coordinate of the block. + /// * `y` - The y-coordinate of the block. + /// * `z` - The z-coordinate of the block. + /// * `block` - The block data to set the block to. + /// + /// # Returns + /// + /// * `Ok(())` - If the block was successfully set. + /// * `Err(WorldError)` - If an error occurs while setting the block. + /// + /// ### Note + /// The positions are modulo'd by 16 to get the block index in the section anyway, so converting + /// the coordinates to section coordinates isn't really necessary, but you should probably do it + /// anyway for readability's sake. + pub fn set_block( + &mut self, + x: i32, + y: i32, + z: i32, + block: BlockData, + ) -> Result<(), WorldError> { + // Get old block + let old_block = self.get_block(x, y, z)?; + if old_block == block { + // debug!("Block is the same as the old block"); + return Ok(()); + } + // Get section + let section = self + .sections + .iter_mut() + .find(|section| section.y == (y >> 4) as i8) + .ok_or(WorldError::SectionOutOfBounds(y >> 4))?; + + let mut converted = false; + let mut new_contents = PaletteType::Indirect { + bits_per_block: 4, + data: vec![], + palette: vec![], + }; + + if let PaletteType::Single(val) = §ion.block_states.block_data { + new_contents = PaletteType::Indirect { + bits_per_block: 4, + data: vec![0; 255], + palette: vec![val.clone()], + }; + converted = true; + } + + if converted { + section.block_states.block_data = new_contents; + } + + // Do different things based on the palette type + match &mut section.block_states.block_data { + PaletteType::Single(_val) => { + panic!("Single palette type should have been converted to indirect palette type"); + } + PaletteType::Indirect { + bits_per_block, + data, + palette, + } => { + // debug!("Indirect mode"); + match section.block_states.block_counts.entry(old_block.clone()) { + Entry::Occupied(mut occ_entry) => { + let count = occ_entry.get_mut(); + if *count <= 0 { + return Err(WorldError::InvalidBlock(old_block)); + } + *count -= 1; + } + Entry::Vacant(empty_entry) => { + warn!("Block not found in block counts: {:?}", old_block); + empty_entry.insert(0); + } + } + let block_id = BLOCK2ID + .get(&block) + .ok_or(WorldError::InvalidBlock(block.clone()))?; + // Add new block + if let Some(e) = section.block_states.block_counts.get(&block) { + section.block_states.block_counts.insert(block, e + 1); + } else { + // debug!("Adding block to block counts"); + section.block_states.block_counts.insert(block, 1); + } + // let required_bits = max((palette.len() as f32).log2().ceil() as u8, 4); + // if *bits_per_block != required_bits { + // section.block_states.resize(required_bits as usize)?; + // } + // Get block index + let block_palette_index = palette + .iter() + .position(|p| p.val == *block_id) + .unwrap_or_else(|| { + // Add block to palette if it doesn't exist + let index = palette.len() as i16; + palette.push((*block_id).into()); + index as usize + }); + // Set block + let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; + let index = ((y & 0xf) * 256 + (z & 0xf) * 16 + (x & 0xf)) as usize; + let i64_index = index / blocks_per_i64; + let packed_u64 = data + .get_mut(i64_index) + .ok_or(InvalidBlockStateData(format!( + "Invalid block state data at index {}", + i64_index + )))?; + let offset = (index % blocks_per_i64) * *bits_per_block as usize; + if let Err(e) = ferrumc_general_purpose::data_packing::u32::write_nbit_u32( + packed_u64, + offset as u32, + block_palette_index as u32, + *bits_per_block, + ) { + return Err(InvalidBlockStateData(format!( + "Failed to write block: {}", + e + ))); + } + } + PaletteType::Direct { .. } => { + todo!("Implement direct palette for set_block"); + } + } + + Ok(()) + } + + /// Gets the block at the specified coordinates. + /// + /// # Arguments + /// + /// * `x` - The x-coordinate of the block. + /// * `y` - The y-coordinate of the block. + /// * `z` - The z-coordinate of the block. + /// + /// # Returns + /// + /// * `Ok(BlockData)` - The block data at the specified coordinates. + /// * `Err(WorldError)` - If an error occurs while retrieving the block data. + /// + /// ### Note + /// The positions are modulo'd by 16 to get the block index in the section anyway, so converting + /// the coordinates to section coordinates isn't really necessary, but you should probably do it + /// anyway for readability's sake. + pub fn get_block(&self, x: i32, y: i32, z: i32) -> Result { + let section = self + .sections + .iter() + .find(|section| section.y == (y >> 4) as i8) + .ok_or(WorldError::SectionOutOfBounds(y >> 4))?; + match §ion.block_states.block_data { + PaletteType::Single(val) => { + let block_id = val.val; + ID2BLOCK + .get(&block_id) + .cloned() + .ok_or(WorldError::ChunkNotFound) + } + PaletteType::Indirect { + bits_per_block, + data, + palette, + } => { + if palette.len() == 1 || *bits_per_block == 0 { + return ID2BLOCK + .get(&palette[0].val) + .cloned() + .ok_or(WorldError::ChunkNotFound); + } + let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; + let index = ((y & 0xf) * 256 + (z & 0xf) * 16 + (x & 0xf)) as usize; + let i64_index = index / blocks_per_i64; + let packed_u64 = data.get(i64_index).ok_or(InvalidBlockStateData(format!( + "Invalid block state data at index {}", + i64_index + )))?; + let offset = (index % blocks_per_i64) * *bits_per_block as usize; + let id = ferrumc_general_purpose::data_packing::u32::read_nbit_u32( + packed_u64, + *bits_per_block, + offset as u32, + )?; + let palette_id = palette.get(id as usize).ok_or(WorldError::ChunkNotFound)?; + Ok(crate::chunk_format::ID2BLOCK + .get(&palette_id.val) + .unwrap_or(&BlockData::default()) + .clone()) + } + &PaletteType::Direct { .. } => todo!("Implement direct palette for get_block"), + } + } + + pub fn new(x: i32, z: i32, dimension: String) -> Self { + let sections: Vec
= (0..24) + .map(|y| Section { + y: y as i8, + block_states: BlockStates { + non_air_blocks: 0, + block_data: PaletteType::Single(VarInt::from(0)), + block_counts: HashMap::from([(BlockData::default(), 4096)]), + }, + biome_states: BiomeStates { + bits_per_biome: 0, + data: vec![], + palette: vec![VarInt::from(0)], + }, + block_light: vec![255; 2048], + sky_light: vec![255; 2048], + }) + .collect(); + // for section in &mut sections { + // section.optimise().expect("Failed to optimise section"); + // } + Chunk { + x, + z, + dimension, + sections, + heightmaps: Heightmaps::new(), + } + } + + /// Sets the section at the specified index to the specified block data. + /// If the section is out of bounds, an error is returned. + /// + /// # Arguments + /// + /// * `section` - The index of the section to set. + /// * `block` - The block data to set the section to. + /// + /// # Returns + /// + /// * `Ok(())` - If the section was successfully set. + /// * `Err(WorldError)` - If an error occurs while setting the section. + pub fn set_section(&mut self, section: u8, block: BlockData) -> Result<(), WorldError> { + if let Some(section) = self.sections.get_mut(section as usize) { + section.fill(block.clone()) + } else { + Err(WorldError::SectionOutOfBounds(section as i32)) + } + } + + /// Fills the chunk with the specified block. + /// + /// # Arguments + /// + /// * `block` - The block data to fill the chunk with. + /// + /// # Returns + /// + /// * `Ok(())` - If the chunk was successfully filled. + /// * `Err(WorldError)` - If an error occurs while filling the chunk. + pub fn fill(&mut self, block: BlockData) -> Result<(), WorldError> { + for section in &mut self.sections { + section.fill(block.clone())?; + } + Ok(()) + } +} + +impl Section { + /// Fills the section with the specified block. + /// + /// # Arguments + /// + /// * `block` - The block data to fill the section with. + /// + /// # Returns + /// + /// * `Ok(())` - If the section was successfully filled. + /// * `Err(WorldError)` - If an error occurs while filling the section. + pub fn fill(&mut self, block: BlockData) -> Result<(), WorldError> { + let block_id = BLOCK2ID + .get(&block) + .ok_or(WorldError::InvalidBlock(block.clone()))?; + self.block_states.block_data = PaletteType::Single(VarInt::from(*block_id)); + self.block_states.block_counts = HashMap::from([(block.clone(), 4096)]); + if ["minecraft:air", "minecraft:void_air", "minecraft:cave_air"] + .contains(&block.name.as_str()) + { + self.block_states.non_air_blocks = 0; + } else { + self.block_states.non_air_blocks = 4096; + } + Ok(()) + } + + /// This function trims out unnecessary data from the section. Primarily it does 2 things: + /// + /// 1. Removes any palette entries that are not used in the block states data. + /// + /// 2. If there is only one block in the palette, it converts the palette to single block mode. + pub fn optimise(&mut self) -> Result<(), WorldError> { + match &mut self.block_states.block_data { + PaletteType::Single(_) => { + // If the section is already in single block mode, there's nothing to optimise + return Ok(()); + } + PaletteType::Indirect { + bits_per_block, + data, + palette, + } => { + // Remove empty blocks from palette + let mut remove_indexes = Vec::new(); + for (block, count) in &self.block_states.block_counts { + if *count <= 0 { + let block_id = BLOCK2ID + .get(block) + .ok_or(WorldError::InvalidBlock(block.clone()))?; + let index = palette.iter().position(|p| p.val == *block_id); + if let Some(index) = index { + remove_indexes.push(index); + } else { + return Err(WorldError::InvalidBlock(block.clone())); + } + } + } + for index in remove_indexes { + // Decrement any data entries that are higher than the removed index + for data_point in &mut *data { + let mut i = 0; + while (i + *bits_per_block as usize) < 64 { + let block_index = + ferrumc_general_purpose::data_packing::u32::read_nbit_u32( + data_point, + *bits_per_block, + i as u32, + )?; + if block_index > index as u32 { + ferrumc_general_purpose::data_packing::u32::write_nbit_u32( + data_point, + i as u32, + block_index - 1, + *bits_per_block, + )?; + } + i += *bits_per_block as usize; + } + } + } + + { + // If there is only one block in the palette, convert to single block mode + if palette.len() == 1 { + let block = ID2BLOCK + .get(&palette[0].val) + .cloned() + .unwrap_or(BlockData::default()); + self.block_states.block_data = PaletteType::Single(palette[0].clone()); + self.block_states.block_counts.clear(); + self.block_states.block_counts.insert(block, 4096); + } + } + } + PaletteType::Direct { .. } => { + todo!("Implement optimisation for direct palette"); + } + }; + + Ok(()) + } +} diff --git a/src/lib/world/src/db_functions.rs b/src/lib/world/src/db_functions.rs index efa69ab3..0ec71098 100644 --- a/src/lib/world/src/db_functions.rs +++ b/src/lib/world/src/db_functions.rs @@ -121,6 +121,16 @@ impl World { } pub(crate) async fn save_chunk_internal(world: &World, chunk: Chunk) -> Result<(), WorldError> { + if !world + .storage_backend + .table_exists("chunks".to_string()) + .await? + { + world + .storage_backend + .create_table("chunks".to_string()) + .await?; + } let as_bytes = world.compressor.compress(&bitcode::encode(&chunk))?; let digest = create_key(chunk.dimension.as_str(), chunk.x, chunk.z); world @@ -209,6 +219,13 @@ pub(crate) async fn chunk_exists_internal( z: i32, dimension: &str, ) -> Result { + if !world + .storage_backend + .table_exists("chunks".to_string()) + .await? + { + return Ok(false); + } let digest = create_key(dimension, x, z); Ok(world .storage_backend diff --git a/src/lib/world/src/edits.rs b/src/lib/world/src/edits.rs new file mode 100644 index 00000000..bad1e41c --- /dev/null +++ b/src/lib/world/src/edits.rs @@ -0,0 +1,85 @@ +use crate::chunk_format::BLOCK2ID; +use crate::errors::WorldError; +use crate::vanilla_chunk_format::BlockData; +use crate::World; +use tracing::debug; + +impl World { + /// Asynchronously retrieves the block data at the specified coordinates in the given dimension. + /// Under the hood, this function just fetches the chunk containing the block and then calls + /// [`crate::chunk_format::Chunk::get_block`] on it. + /// + /// # Arguments + /// + /// * `x` - The x-coordinate of the block. + /// * `y` - The y-coordinate of the block. + /// * `z` - The z-coordinate of the block. + /// * `dimension` - The dimension in which the block is located. + /// + /// # Returns + /// + /// * `Ok(BlockData)` - The block data at the specified coordinates. + /// * `Err(WorldError)` - If an error occurs while retrieving the block data. + /// + /// # Errors + /// + /// * `WorldError::SectionOutOfBounds` - If the section containing the block is out of bounds. + /// * `WorldError::ChunkNotFound` - If the chunk or block data is not found. + /// * `WorldError::InvalidBlockStateData` - If the block state data is invalid. + pub async fn get_block_and_fetch( + &self, + x: i32, + y: i32, + z: i32, + dimension: &str, + ) -> Result { + let chunk_x = x >> 4; + let chunk_z = z >> 4; + let chunk = self.load_chunk(chunk_x, chunk_z, dimension).await?; + chunk.get_block(x, y, z) + } + + /// Asynchronously sets the block data at the specified coordinates in the given dimension. + /// Under the hood, this function just fetches the chunk containing the block and then calls + /// [`crate::chunk_format::Chunk::set_block`] on it. + /// + /// # Arguments + /// + /// * `x` - The x-coordinate of the block. + /// * `y` - The y-coordinate of the block. + /// * `z` - The z-coordinate of the block. + /// * `dimension` - The dimension in which the block is located. + /// * `block` - The block data to set. + /// + /// # Returns + /// + /// * `Ok(())` - If the block data is successfully set. + /// * `Err(WorldError)` - If an error occurs while setting the block data. + pub async fn set_block_and_fetch( + &self, + x: i32, + y: i32, + z: i32, + dimension: &str, + block: BlockData, + ) -> Result<(), WorldError> { + if !BLOCK2ID.contains_key(&block) { + return Err(WorldError::InvalidBlock(block)); + }; + // Get chunk + let chunk_x = x >> 4; + let chunk_z = z >> 4; + let mut chunk = self.load_chunk(chunk_x, chunk_z, dimension).await?; + + debug!("Chunk: {}, {}", chunk_x, chunk_z); + + chunk.set_block(x, y, z, block)?; + for section in &mut chunk.sections { + section.optimise()?; + } + + // Save chunk + self.save_chunk(chunk).await?; + Ok(()) + } +} diff --git a/src/lib/world/src/errors.rs b/src/lib/world/src/errors.rs index 7fcb6ad6..998c8bcf 100644 --- a/src/lib/world/src/errors.rs +++ b/src/lib/world/src/errors.rs @@ -1,7 +1,8 @@ use crate::errors::WorldError::{GenericIOError, PermissionError}; -use crate::vanilla_chunk_format::Palette; +use crate::vanilla_chunk_format::BlockData; use errors::AnvilError; use ferrumc_anvil::errors; +use ferrumc_general_purpose::data_packing::errors::DataPackingError; use ferrumc_storage::errors::StorageError; use std::io::ErrorKind; use thiserror::Error; @@ -35,11 +36,17 @@ pub enum WorldError { #[error("Anvil Decode Error: {0}")] AnvilDecodeError(AnvilError), #[error("Missing block mapping: {0}")] - MissingBlockMapping(Palette), + MissingBlockMapping(BlockData), #[error("Invalid memory map size: {0}")] InvalidMapSize(u64), #[error("Task Join Error: {0}")] TaskJoinError(String), + #[error("Section out of bounds: {0}")] + SectionOutOfBounds(i32), + #[error("Invalid block state data")] + InvalidBlockStateData(String), + #[error("Invalid block: {0}")] + InvalidBlock(BlockData), } // implemente AcquireError for WorldError @@ -71,3 +78,9 @@ impl From for WorldError { WorldError::AnvilDecodeError(err) } } + +impl From for WorldError { + fn from(e: DataPackingError) -> Self { + WorldError::InvalidBlockStateData(e.to_string()) + } +} diff --git a/src/lib/world/src/lib.rs b/src/lib/world/src/lib.rs index d4736b95..a58afdd0 100644 --- a/src/lib/world/src/lib.rs +++ b/src/lib/world/src/lib.rs @@ -2,9 +2,10 @@ pub mod chunk_format; mod db_functions; +pub mod edits; pub mod errors; mod importing; -mod vanilla_chunk_format; +pub mod vanilla_chunk_format; use crate::chunk_format::Chunk; use crate::errors::WorldError; diff --git a/src/lib/world/src/vanilla_chunk_format.rs b/src/lib/world/src/vanilla_chunk_format.rs index 5cdfb149..4f25a758 100644 --- a/src/lib/world/src/vanilla_chunk_format.rs +++ b/src/lib/world/src/vanilla_chunk_format.rs @@ -95,24 +95,33 @@ pub(crate) struct Section { #[derive(deepsize::DeepSizeOf)] pub(crate) struct BlockStates { pub data: Option>, - pub palette: Option>, + pub palette: Option>, } #[apply(ChunkDerives)] #[derive(deepsize::DeepSizeOf, Hash)] -pub struct Palette { +pub struct BlockData { #[nbt(rename = "Name")] pub name: String, #[nbt(rename = "Properties")] pub properties: Option>, } -impl Display for Palette { +impl Display for BlockData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) } } +impl Default for BlockData { + fn default() -> Self { + BlockData { + name: String::from("minecraft:air"), + properties: None, + } + } +} + #[apply(ChunkDerives)] #[derive(deepsize::DeepSizeOf)] pub(crate) struct Properties {