diff --git a/Cargo.toml b/Cargo.toml index e35e93c96..cf0148b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ glam = "0.23.0" heck = "0.4.0" hmac = "0.12.1" indexmap = "1.9.3" +lru = "0.10.0" noise = "0.8.2" num = "0.4.0" num-bigint = "0.4.3" diff --git a/crates/README.md b/crates/README.md index 7c4f8c662..7a63625c3 100644 --- a/crates/README.md +++ b/crates/README.md @@ -10,7 +10,7 @@ Ignoring transitive dependencies and `valence_core`, the dependency graph can be ```mermaid graph TD - network --> client + network --> client client --> instance biome --> registry dimension --> registry @@ -19,7 +19,7 @@ graph TD instance --> entity player_list --> client inventory --> client - anvil --> instance + anvil --> client entity --> block advancement --> client world_border --> client diff --git a/crates/valence/benches/anvil.rs b/crates/valence/benches/anvil.rs index 327bced4f..77679473b 100644 --- a/crates/valence/benches/anvil.rs +++ b/crates/valence/benches/anvil.rs @@ -1,3 +1,4 @@ +/* use std::fs::create_dir_all; use std::hint::black_box; use std::path::{Path, PathBuf}; @@ -133,3 +134,4 @@ fn get_world_asset( Ok(final_path) } +*/ diff --git a/crates/valence/benches/main.rs b/crates/valence/benches/main.rs index 06fdb582e..6f7524bcd 100644 --- a/crates/valence/benches/main.rs +++ b/crates/valence/benches/main.rs @@ -10,7 +10,7 @@ mod var_long; criterion_group! { benches, - anvil::load, + // anvil::load, block::block, decode_array::decode_array, idle::idle_update, diff --git a/crates/valence/examples/anvil_loading.rs b/crates/valence/examples/anvil_loading.rs index bc97f6f27..f79fdb3d2 100644 --- a/crates/valence/examples/anvil_loading.rs +++ b/crates/valence/examples/anvil_loading.rs @@ -1,80 +1,44 @@ -use std::collections::hash_map::Entry; -use std::collections::HashMap; use std::path::PathBuf; -use std::thread; use clap::Parser; -use flume::{Receiver, Sender}; -use tracing::warn; -use valence::anvil::{AnvilChunk, AnvilWorld}; use valence::prelude::*; +use valence_anvil::{AnvilLevel, ChunkLoadEvent, ChunkLoadStatus}; +use valence_client::message::SendMessage; const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0); -const SECTION_COUNT: usize = 24; -#[derive(Parser)] +#[derive(Parser, Resource)] #[clap(author, version, about)] struct Cli { /// The path to a Minecraft world save containing a `region` subdirectory. path: PathBuf, } -#[derive(Resource)] -struct GameState { - /// Chunks that need to be generated. Chunks without a priority have already - /// been sent to the anvil thread. - pending: HashMap>, - sender: Sender, - receiver: Receiver<(ChunkPos, Chunk)>, -} - -/// The order in which chunks should be processed by anvil worker. Smaller -/// values are sent first. -type Priority = u64; - pub fn main() { tracing_subscriber::fmt().init(); let cli = Cli::parse(); - let dir = cli.path; - if !dir.exists() { - eprintln!("Directory `{}` does not exist. Exiting.", dir.display()); - return; - } else if !dir.is_dir() { - eprintln!("`{}` is not a directory. Exiting.", dir.display()); + if !cli.path.exists() { + eprintln!( + "Directory `{}` does not exist. Exiting.", + cli.path.display() + ); return; } - let anvil = AnvilWorld::new(dir); - - let (finished_sender, finished_receiver) = flume::unbounded(); - let (pending_sender, pending_receiver) = flume::unbounded(); - - // Process anvil chunks in a different thread to avoid blocking the main tick - // loop. - thread::spawn(move || anvil_worker(pending_receiver, finished_sender, anvil)); - - let game_state = GameState { - pending: HashMap::new(), - sender: pending_sender, - receiver: finished_receiver, - }; + if !cli.path.is_dir() { + eprintln!("`{}` is not a directory. Exiting.", cli.path.display()); + return; + } App::new() .add_plugins(DefaultPlugins) - .insert_resource(game_state) + .insert_resource(cli) .add_startup_system(setup) - .add_systems( - ( - init_clients, - remove_unviewed_chunks, - update_client_views, - send_recv_chunks, - ) - .chain(), - ) .add_system(despawn_disconnected_clients) + .add_systems((init_clients, handle_chunk_loads).chain()) + .add_system(display_loaded_chunk_count) .run(); } @@ -83,9 +47,24 @@ fn setup( dimensions: Res, biomes: Res, server: Res, + cli: Res, ) { let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); - commands.spawn(instance); + let mut level = AnvilLevel::new(&cli.path, &biomes); + + // Force a 16x16 area of chunks around the origin to be loaded at all times, + // similar to spawn chunks in vanilla. This isn't necessary, but it is done to + // demonstrate that it is possible. + for z in -8..8 { + for x in -8..8 { + let pos = ChunkPos::new(x, z); + + level.ignored_chunks.insert(pos); + level.force_chunk_load(pos); + } + } + + commands.spawn((instance, level)); } fn init_clients( @@ -100,103 +79,44 @@ fn init_clients( } } -fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) { - instances - .single_mut() - .retain_chunks(|_, chunk| chunk.is_viewed_mut()); -} - -fn update_client_views( - mut instances: Query<&mut Instance>, - mut clients: Query<(&mut Client, View, OldView)>, - mut state: ResMut, +fn handle_chunk_loads( + mut events: EventReader, + mut instances: Query<&mut Instance, With>, ) { - let instance = instances.single_mut(); - - for (client, view, old_view) in &mut clients { - let view = view.get(); - let queue_pos = |pos| { - if instance.chunk(pos).is_none() { - match state.pending.entry(pos) { - Entry::Occupied(mut oe) => { - if let Some(priority) = oe.get_mut() { - let dist = view.pos.distance_squared(pos); - *priority = (*priority).min(dist); - } - } - Entry::Vacant(ve) => { - let dist = view.pos.distance_squared(pos); - ve.insert(Some(dist)); - } - } + let mut inst = instances.single_mut(); + + for event in events.iter() { + match &event.status { + ChunkLoadStatus::Success { .. } => { + // The chunk was inserted into the world. Nothing for us to do. } - }; - - // Queue all the new chunks in the view to be sent to the anvil worker. - if client.is_added() { - view.iter().for_each(queue_pos); - } else { - let old_view = old_view.get(); - if old_view != view { - view.diff(old_view).for_each(queue_pos); + ChunkLoadStatus::Empty => { + // There's no chunk here so let's insert an empty chunk. If we were doing + // terrain generation we would prepare that here. + inst.insert_chunk(event.pos, Chunk::default()); } - } - } -} - -fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut) { - let mut instance = instances.single_mut(); - let state = state.into_inner(); - - // Insert the chunks that are finished loading into the instance. - for (pos, chunk) in state.receiver.drain() { - instance.insert_chunk(pos, chunk); - assert!(state.pending.remove(&pos).is_some()); - } - - // Collect all the new chunks that need to be loaded this tick. - let mut to_send = vec![]; - - for (pos, priority) in &mut state.pending { - if let Some(pri) = priority.take() { - to_send.push((pri, pos)); - } - } - - // Sort chunks by ascending priority. - to_send.sort_unstable_by_key(|(pri, _)| *pri); - - // Send the sorted chunks to be loaded. - for (_, pos) in to_send { - let _ = state.sender.try_send(*pos); - } -} - -fn anvil_worker( - receiver: Receiver, - sender: Sender<(ChunkPos, Chunk)>, - mut world: AnvilWorld, -) { - while let Ok(pos) = receiver.recv() { - match get_chunk(pos, &mut world) { - Ok(chunk) => { - if let Some(chunk) = chunk { - let _ = sender.try_send((pos, chunk)); - } + ChunkLoadStatus::Failed(e) => { + // Something went wrong. + eprintln!( + "failed to load chunk at ({}, {}): {e:#}", + event.pos.x, event.pos.z + ); + inst.insert_chunk(event.pos, Chunk::default()); } - Err(e) => warn!("Failed to get chunk at ({}, {}): {e:#}.", pos.x, pos.z), } } } -fn get_chunk(pos: ChunkPos, world: &mut AnvilWorld) -> anyhow::Result> { - let Some(AnvilChunk { data, .. }) = world.read_chunk(pos.x, pos.z)? else { - return Ok(None) - }; +// Display the number of loaded chunks in the action bar of all clients. +fn display_loaded_chunk_count(mut instances: Query<&mut Instance>, mut last_count: Local) { + let mut inst = instances.single_mut(); - let mut chunk = Chunk::new(SECTION_COUNT); + let cnt = inst.chunks().count(); - valence_anvil::to_valence(&data, &mut chunk, 4, |_| BiomeId::default())?; - - Ok(Some(chunk)) + if *last_count != cnt { + *last_count = cnt; + inst.send_action_bar_message( + "Chunk Count: ".into_text() + (cnt as i32).color(Color::LIGHT_PURPLE), + ); + } } diff --git a/crates/valence/examples/block_entities.rs b/crates/valence/examples/block_entities.rs index 36b26d75d..2e00dd94f 100644 --- a/crates/valence/examples/block_entities.rs +++ b/crates/valence/examples/block_entities.rs @@ -2,8 +2,8 @@ use valence::nbt::{compound, List}; use valence::prelude::*; -use valence_client::chat::ChatMessageEvent; use valence_client::interact_block::InteractBlockEvent; +use valence_client::message::ChatMessageEvent; const FLOOR_Y: i32 = 64; const SIGN_POS: [i32; 3] = [3, FLOOR_Y + 1, 2]; diff --git a/crates/valence/examples/building.rs b/crates/valence/examples/building.rs index f4bb404ab..9882d32a4 100644 --- a/crates/valence/examples/building.rs +++ b/crates/valence/examples/building.rs @@ -3,6 +3,7 @@ use valence::inventory::HeldItem; use valence::prelude::*; use valence_client::interact_block::InteractBlockEvent; +use valence_client::message::SendMessage; const SPAWN_Y: i32 = 64; @@ -55,7 +56,7 @@ fn init_clients( loc.0 = instances.single(); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); - client.send_message("Welcome to Valence! Build something cool.".italic()); + client.send_chat_message("Welcome to Valence! Build something cool.".italic()); } } diff --git a/crates/valence/examples/conway.rs b/crates/valence/examples/conway.rs index 511a329f6..10aefe667 100644 --- a/crates/valence/examples/conway.rs +++ b/crates/valence/examples/conway.rs @@ -3,6 +3,7 @@ use std::mem; use valence::prelude::*; +use valence_client::message::SendMessage; const BOARD_MIN_X: i32 = -30; const BOARD_MAX_X: i32 = 30; @@ -74,8 +75,8 @@ fn init_clients( instances: Query>, ) { for (mut client, mut loc, mut pos) in &mut clients { - client.send_message("Welcome to Conway's game of life in Minecraft!".italic()); - client.send_message( + client.send_chat_message("Welcome to Conway's game of life in Minecraft!".italic()); + client.send_chat_message( "Sneak to toggle running the simulation and the left mouse button to bring blocks to \ life." .italic(), diff --git a/crates/valence/examples/death.rs b/crates/valence/examples/death.rs index 621301098..d65f10ad4 100644 --- a/crates/valence/examples/death.rs +++ b/crates/valence/examples/death.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] use valence::prelude::*; +use valence_client::message::SendMessage; use valence_client::status::RequestRespawnEvent; const SPAWN_Y: i32 = 64; @@ -58,7 +59,7 @@ fn init_clients( pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]); has_respawn_screen.0 = true; - client.send_message( + client.send_chat_message( "Welcome to Valence! Sneak to die in the game (but not in real life).".italic(), ); } diff --git a/crates/valence/examples/entity_hitbox.rs b/crates/valence/examples/entity_hitbox.rs index 8b17eea8c..47a86c735 100644 --- a/crates/valence/examples/entity_hitbox.rs +++ b/crates/valence/examples/entity_hitbox.rs @@ -4,6 +4,7 @@ use bevy_app::App; use bevy_ecs::prelude::Entity; use rand::Rng; use valence::prelude::*; +use valence_client::message::SendMessage; use valence_entity::entity::NameVisible; use valence_entity::hoglin::HoglinEntityBundle; use valence_entity::pig::PigEntityBundle; @@ -53,7 +54,7 @@ fn init_clients( loc.0 = instances.single(); pos.set([0.5, 65.0, 0.5]); *game_mode = GameMode::Creative; - client.send_message("To spawn an entity, press shift. F3 + B to activate hitboxes"); + client.send_chat_message("To spawn an entity, press shift. F3 + B to activate hitboxes"); } } diff --git a/crates/valence/examples/parkour.rs b/crates/valence/examples/parkour.rs index e0c5f38f2..0dc873140 100644 --- a/crates/valence/examples/parkour.rs +++ b/crates/valence/examples/parkour.rs @@ -7,6 +7,7 @@ use rand::seq::SliceRandom; use rand::Rng; use valence::prelude::*; use valence::protocol::packet::sound::{Sound, SoundCategory}; +use valence_client::message::SendMessage; const START_POS: BlockPos = BlockPos::new(0, 100, 0); const VIEW_DIST: u8 = 10; @@ -66,7 +67,7 @@ fn init_clients( is_flat.0 = true; *game_mode = GameMode::Adventure; - client.send_message("Welcome to epic infinite parkour game!".italic()); + client.send_chat_message("Welcome to epic infinite parkour game!".italic()); let state = GameState { blocks: VecDeque::new(), @@ -96,7 +97,7 @@ fn reset_clients( if out_of_bounds || state.is_added() { if out_of_bounds && !state.is_added() { - client.send_message( + client.send_chat_message( "Your score was ".italic() + state .score diff --git a/crates/valence/examples/player_list.rs b/crates/valence/examples/player_list.rs index 77ba13317..0b50f7f5b 100644 --- a/crates/valence/examples/player_list.rs +++ b/crates/valence/examples/player_list.rs @@ -3,6 +3,7 @@ use rand::Rng; use valence::player_list::{DisplayName, PlayerListEntryBundle}; use valence::prelude::*; +use valence_client::message::SendMessage; use valence_client::Ping; const SPAWN_Y: i32 = 64; @@ -62,7 +63,7 @@ fn init_clients( loc.0 = instances.single(); *game_mode = GameMode::Creative; - client.send_message( + client.send_chat_message( "Please open your player list (tab key)." .italic() .color(Color::WHITE), diff --git a/crates/valence/examples/resource_pack.rs b/crates/valence/examples/resource_pack.rs index ccbac6571..5701d0849 100644 --- a/crates/valence/examples/resource_pack.rs +++ b/crates/valence/examples/resource_pack.rs @@ -3,6 +3,7 @@ use valence::entity::player::PlayerEntityBundle; use valence::entity::sheep::SheepEntityBundle; use valence::prelude::*; +use valence_client::message::SendMessage; use valence_client::resource_pack::{ResourcePackStatus, ResourcePackStatusEvent}; const SPAWN_Y: i32 = 64; @@ -57,7 +58,7 @@ fn init_clients( for (entity, uuid, mut client, mut game_mode) in &mut clients { *game_mode = GameMode::Creative; - client.send_message("Hit the sheep to prompt for the resource pack.".italic()); + client.send_chat_message("Hit the sheep to prompt for the resource pack.".italic()); commands.entity(entity).insert(PlayerEntityBundle { location: Location(instances.single()), @@ -91,17 +92,18 @@ fn on_resource_pack_status( if let Ok(mut client) = clients.get_mut(event.client) { match event.status { ResourcePackStatus::Accepted => { - client.send_message("Resource pack accepted.".color(Color::GREEN)); + client.send_chat_message("Resource pack accepted.".color(Color::GREEN)); } ResourcePackStatus::Declined => { - client.send_message("Resource pack declined.".color(Color::RED)); + client.send_chat_message("Resource pack declined.".color(Color::RED)); } ResourcePackStatus::FailedDownload => { - client.send_message("Resource pack failed to download.".color(Color::RED)); + client.send_chat_message("Resource pack failed to download.".color(Color::RED)); } ResourcePackStatus::SuccessfullyLoaded => { - client - .send_message("Resource pack successfully downloaded.".color(Color::BLUE)); + client.send_chat_message( + "Resource pack successfully downloaded.".color(Color::BLUE), + ); } } }; diff --git a/crates/valence/examples/text.rs b/crates/valence/examples/text.rs index a369cd7ad..a990ecb13 100644 --- a/crates/valence/examples/text.rs +++ b/crates/valence/examples/text.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] use valence::prelude::*; +use valence_client::message::SendMessage; const SPAWN_Y: i32 = 64; @@ -47,16 +48,17 @@ fn init_clients( loc.0 = instances.single(); *game_mode = GameMode::Creative; - client.send_message("Welcome to the text example.".bold()); - client - .send_message("The following examples show ways to use the different text components."); + client.send_chat_message("Welcome to the text example.".bold()); + client.send_chat_message( + "The following examples show ways to use the different text components.", + ); // Text examples - client.send_message("\nText"); - client.send_message(" - ".into_text() + Text::text("Plain text")); - client.send_message(" - ".into_text() + Text::text("Styled text").italic()); - client.send_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD)); - client.send_message( + client.send_chat_message("\nText"); + client.send_chat_message(" - ".into_text() + Text::text("Plain text")); + client.send_chat_message(" - ".into_text() + Text::text("Styled text").italic()); + client.send_chat_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD)); + client.send_chat_message( " - ".into_text() + Text::text("Colored and styled text") .color(Color::GOLD) @@ -65,59 +67,61 @@ fn init_clients( ); // Translated text examples - client.send_message("\nTranslated Text"); - client.send_message( + client.send_chat_message("\nTranslated Text"); + client.send_chat_message( " - 'chat.type.advancement.task': ".into_text() + Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []), ); - client.send_message( + client.send_chat_message( " - 'chat.type.advancement.task' with slots: ".into_text() + Text::translate( translation_key::CHAT_TYPE_ADVANCEMENT_TASK, ["arg1".into(), "arg2".into()], ), ); - client.send_message( + client.send_chat_message( " - 'custom.translation_key': ".into_text() + Text::translate("custom.translation_key", []), ); // Scoreboard value example - client.send_message("\nScoreboard Values"); - client.send_message(" - Score: ".into_text() + Text::score("*", "objective", None)); - client.send_message( + client.send_chat_message("\nScoreboard Values"); + client.send_chat_message(" - Score: ".into_text() + Text::score("*", "objective", None)); + client.send_chat_message( " - Score with custom value: ".into_text() + Text::score("*", "objective", Some("value".into())), ); // Entity names example - client.send_message("\nEntity Names (Selector)"); - client.send_message(" - Nearest player: ".into_text() + Text::selector("@p", None)); - client.send_message(" - Random player: ".into_text() + Text::selector("@r", None)); - client.send_message(" - All players: ".into_text() + Text::selector("@a", None)); - client.send_message(" - All entities: ".into_text() + Text::selector("@e", None)); - client.send_message( + client.send_chat_message("\nEntity Names (Selector)"); + client.send_chat_message(" - Nearest player: ".into_text() + Text::selector("@p", None)); + client.send_chat_message(" - Random player: ".into_text() + Text::selector("@r", None)); + client.send_chat_message(" - All players: ".into_text() + Text::selector("@a", None)); + client.send_chat_message(" - All entities: ".into_text() + Text::selector("@e", None)); + client.send_chat_message( " - All entities with custom separator: ".into_text() + Text::selector("@e", Some(", ".into_text().color(Color::GOLD))), ); // Keybind example - client.send_message("\nKeybind"); - client.send_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory")); + client.send_chat_message("\nKeybind"); + client + .send_chat_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory")); // NBT examples - client.send_message("\nNBT"); - client.send_message( + client.send_chat_message("\nNBT"); + client.send_chat_message( " - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None), ); - client - .send_message(" - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None)); - client.send_message( + client.send_chat_message( + " - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None), + ); + client.send_chat_message( " - Storage NBT: ".into_text() + Text::storage_nbt(ident!("storage.key"), "@a", None, None), ); - client.send_message( + client.send_chat_message( "\n\n↑ ".into_text().bold().color(Color::GOLD) + "Scroll up to see the full example!".into_text().not_bold(), ); diff --git a/crates/valence/examples/world_border.rs b/crates/valence/examples/world_border.rs index 52f71b8ca..8f908708c 100644 --- a/crates/valence/examples/world_border.rs +++ b/crates/valence/examples/world_border.rs @@ -1,11 +1,12 @@ use std::time::Duration; use bevy_app::App; -use valence::client::chat::ChatMessageEvent; use valence::client::despawn_disconnected_clients; +use valence::client::message::ChatMessageEvent; use valence::inventory::HeldItem; use valence::prelude::*; use valence::world_border::*; +use valence_client::message::SendMessage; const SPAWN_Y: i32 = 64; @@ -66,7 +67,7 @@ fn init_clients( pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); let pickaxe = Some(ItemStack::new(ItemKind::WoodenPickaxe, 1, None)); inv.set_slot(main_slot.slot(), pickaxe); - client.send_message("Break block to increase border size!"); + client.send_chat_message("Break block to increase border size!"); } } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index a33b35e17..9ea686615 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -154,7 +154,7 @@ impl PluginGroup for DefaultPlugins { #[cfg(feature = "anvil")] { - // No plugin... yet. + group = group.add(valence_anvil::AnvilPlugin); } #[cfg(feature = "advancement")] diff --git a/crates/valence_anvil/Cargo.toml b/crates/valence_anvil/Cargo.toml index 5c5319f8a..4031233bd 100644 --- a/crates/valence_anvil/Cargo.toml +++ b/crates/valence_anvil/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "valence_anvil" -description = "A library for Minecraft's Anvil world format." +description = "Anvil world format support for Valence" documentation.workspace = true -repository = "https://github.com/valence-rs/valence/tree/main/crates/valence_anvil" readme = "README.md" license.workspace = true keywords = ["anvil", "minecraft", "deserialization"] @@ -10,12 +9,20 @@ version.workspace = true edition.workspace = true [dependencies] +anyhow.workspace = true +bevy_app.workspace = true +bevy_ecs.workspace = true byteorder.workspace = true flate2.workspace = true +flume.workspace = true +lru.workspace = true num-integer.workspace = true thiserror.workspace = true +tracing.workspace = true valence_biome.workspace = true valence_block.workspace = true +valence_client.workspace = true valence_core.workspace = true +valence_entity.workspace = true valence_instance.workspace = true valence_nbt.workspace = true diff --git a/crates/valence_anvil/src/lib.rs b/crates/valence_anvil/src/lib.rs index 5b5d31e24..f08f17afa 100644 --- a/crates/valence_anvil/src/lib.rs +++ b/crates/valence_anvil/src/lib.rs @@ -17,99 +17,149 @@ clippy::dbg_macro )] -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; +use std::collections::hash_map::Entry; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fs::File; -use std::io; use std::io::{ErrorKind, Read, Seek, SeekFrom}; +use std::num::NonZeroUsize; use std::path::PathBuf; +use std::thread; +use anyhow::{bail, ensure}; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; use byteorder::{BigEndian, ReadBytesExt}; use flate2::bufread::{GzDecoder, ZlibDecoder}; -use thiserror::Error; -pub use to_valence::*; +use flume::{Receiver, Sender}; +use lru::LruCache; +use tracing::warn; +use valence_biome::{BiomeId, BiomeRegistry}; +use valence_client::{Client, OldView, UpdateClientsSet, View}; +use valence_core::chunk_pos::ChunkPos; +use valence_core::ident::Ident; +use valence_entity::{Location, OldLocation}; +use valence_instance::{Chunk, Instance}; use valence_nbt::Compound; -mod to_valence; - -#[derive(Debug)] -pub struct AnvilWorld { - /// Path to the "region" subdirectory in the world root. - region_root: PathBuf, - // TODO: LRU cache for region file handles. - /// Maps region (x, z) positions to region files. - regions: BTreeMap<(i32, i32), Region>, -} - -#[derive(Clone, PartialEq, Debug)] -pub struct AnvilChunk { - /// This chunk's NBT data. - pub data: Compound, - /// The time this chunk was last modified measured in seconds since the - /// epoch. - pub timestamp: u32, -} - -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum ReadChunkError { - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Nbt(#[from] valence_nbt::binary::Error), - #[error("invalid chunk sector offset")] - BadSectorOffset, - #[error("invalid chunk size")] - BadChunkSize, - #[error("unknown compression scheme number of {0}")] - UnknownCompressionScheme(u8), - #[error("not all chunk NBT data was read")] - IncompleteNbtRead, -} - -#[derive(Debug)] -struct Region { - file: File, - /// The first 8 KiB in the file. - header: [u8; SECTOR_SIZE * 2], +mod parse_chunk; + +#[derive(Component, Debug)] +pub struct AnvilLevel { + /// Chunk worker state to be moved to another thread. + worker_state: Option, + /// The set of chunk positions that should not be loaded or unloaded by + /// the anvil system. + /// + /// This set is empty by default, but you can modify it at any time. + pub ignored_chunks: HashSet, + /// Chunks that need to be loaded. Chunks with `None` priority have already + /// been sent to the anvil thread. + pending: HashMap>, + /// Sender for the chunk worker thread. + sender: Sender, + /// Receiver for the chunk worker thread. + receiver: Receiver<(ChunkPos, WorkerResult)>, } -const SECTOR_SIZE: usize = 4096; +type WorkerResult = anyhow::Result>; -impl AnvilWorld { - pub fn new(world_root: impl Into) -> Self { +impl AnvilLevel { + pub fn new(world_root: impl Into, biomes: &BiomeRegistry) -> Self { let mut region_root = world_root.into(); region_root.push("region"); + let (pending_sender, pending_receiver) = flume::unbounded(); + let (finished_sender, finished_receiver) = flume::bounded(4096); + Self { - region_root, - regions: BTreeMap::new(), + worker_state: Some(ChunkWorkerState { + regions: LruCache::new(LRU_CACHE_SIZE), + region_root, + sender: finished_sender, + receiver: pending_receiver, + decompress_buf: vec![], + biome_to_id: biomes + .iter() + .map(|(id, name, _)| (name.to_string_ident(), id)) + .collect(), + section_count: 0, // Assigned later. + }), + ignored_chunks: HashSet::new(), + pending: HashMap::new(), + sender: pending_sender, + receiver: finished_receiver, } } - /// Reads a chunk from the file system with the given chunk coordinates. If - /// no chunk exists at the position, then `None` is returned. - pub fn read_chunk( - &mut self, - chunk_x: i32, - chunk_z: i32, - ) -> Result, ReadChunkError> { - let region_x = chunk_x.div_euclid(32); - let region_z = chunk_z.div_euclid(32); - - let region = match self.regions.entry((region_x, region_z)) { + /// Forces a chunk to be loaded at a specific position in this world. This + /// will bypass [`AnvilLevel::ignored_chunks`]. + /// Note that the chunk will be unloaded next tick unless it has been added + /// to [`AnvilLevel::ignored_chunks`] or it is in view of a client. + /// + /// This has no effect if a chunk at the position is already present. + pub fn force_chunk_load(&mut self, pos: ChunkPos) { + match self.pending.entry(pos) { + Entry::Occupied(oe) => { + // If the chunk is already scheduled to load but hasn't been sent to the chunk + // worker yet, then give it the highest priority. + if let Some(priority) = oe.into_mut() { + *priority = 0; + } + } Entry::Vacant(ve) => { - // Load the region file if it exists. Otherwise, the chunk is considered absent. + ve.insert(Some(0)); + } + } + } +} + +const LRU_CACHE_SIZE: NonZeroUsize = match NonZeroUsize::new(256) { + Some(n) => n, + None => unreachable!(), +}; + +/// The order in which chunks should be processed by the anvil worker. Smaller +/// values are sent first. +type Priority = u64; + +#[derive(Debug)] +struct ChunkWorkerState { + /// Region files. An LRU cache is used to limit the number of open file + /// handles. + regions: LruCache, + /// Path to the "region" subdirectory in the world root. + region_root: PathBuf, + /// Sender of finished chunks. + sender: Sender<(ChunkPos, WorkerResult)>, + /// Receiver of pending chunks. + receiver: Receiver, + /// Scratch buffer for decompression. + decompress_buf: Vec, + /// Mapping of biome names to their biome ID. + biome_to_id: BTreeMap, BiomeId>, + /// Number of chunk sections in the instance. + section_count: usize, +} - // TODO: Add tombstone for missing region file in `regions`. +impl ChunkWorkerState { + fn get_chunk(&mut self, pos: ChunkPos) -> anyhow::Result> { + let region_x = pos.x.div_euclid(32); + let region_z = pos.z.div_euclid(32); + let region = match self.regions.get_mut(&(region_x, region_z)) { + Some(RegionEntry::Occupied(region)) => region, + Some(RegionEntry::Vacant) => return Ok(None), + None => { let path = self .region_root .join(format!("r.{region_x}.{region_z}.mca")); let mut file = match File::options().read(true).write(true).open(path) { Ok(file) => file, - Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None), + Err(e) if e.kind() == ErrorKind::NotFound => { + self.regions.put((region_x, region_z), RegionEntry::Vacant); + return Ok(None); + } Err(e) => return Err(e.into()), }; @@ -117,12 +167,19 @@ impl AnvilWorld { file.read_exact(&mut header)?; - ve.insert(Region { file, header }) + // TODO: this is ugly. + let res = self.regions.get_or_insert_mut((region_x, region_z), || { + RegionEntry::Occupied(Region { file, header }) + }); + + match res { + RegionEntry::Occupied(r) => r, + RegionEntry::Vacant => unreachable!(), + } } - Entry::Occupied(oe) => oe.into_mut(), }; - let chunk_idx = (chunk_x.rem_euclid(32) + chunk_z.rem_euclid(32) * 32) as usize; + let chunk_idx = (pos.x.rem_euclid(32) + pos.z.rem_euclid(32) * 32) as usize; let location_bytes = (®ion.header[chunk_idx * 4..]).read_u32::()?; let timestamp = (®ion.header[chunk_idx * 4 + SECTOR_SIZE..]).read_u32::()?; @@ -135,11 +192,9 @@ impl AnvilWorld { let sector_offset = (location_bytes >> 8) as u64; let sector_count = (location_bytes & 0xff) as usize; - if sector_offset < 2 { - // If the sector offset was <2, then the chunk data would be inside the region - // header. That doesn't make any sense. - return Err(ReadChunkError::BadSectorOffset); - } + // If the sector offset was <2, then the chunk data would be inside the region + // header. That doesn't make any sense. + ensure!(sector_offset >= 2, "invalid chunk sector offset"); // Seek to the beginning of the chunk's data. region @@ -148,44 +203,267 @@ impl AnvilWorld { let exact_chunk_size = region.file.read_u32::()? as usize; - if exact_chunk_size > sector_count * SECTOR_SIZE { - // Sector size of this chunk must always be >= the exact size. - return Err(ReadChunkError::BadChunkSize); - } + // size of this chunk in sectors must always be >= the exact size. + ensure!( + sector_count * SECTOR_SIZE >= exact_chunk_size, + "invalid chunk size" + ); let mut data_buf = vec![0; exact_chunk_size].into_boxed_slice(); region.file.read_exact(&mut data_buf)?; let mut r = data_buf.as_ref(); - let mut decompress_buf = vec![]; + self.decompress_buf.clear(); // What compression does the chunk use? let mut nbt_slice = match r.read_u8()? { // GZip 1 => { let mut z = GzDecoder::new(r); - z.read_to_end(&mut decompress_buf)?; - decompress_buf.as_slice() + z.read_to_end(&mut self.decompress_buf)?; + self.decompress_buf.as_slice() } // Zlib 2 => { let mut z = ZlibDecoder::new(r); - z.read_to_end(&mut decompress_buf)?; - decompress_buf.as_slice() + z.read_to_end(&mut self.decompress_buf)?; + self.decompress_buf.as_slice() } // Uncompressed 3 => r, // Unknown - b => return Err(ReadChunkError::UnknownCompressionScheme(b)), + b => bail!("unknown compression scheme number of {b}"), }; let (data, _) = Compound::from_binary(&mut nbt_slice)?; - if !nbt_slice.is_empty() { - return Err(ReadChunkError::IncompleteNbtRead); - } + ensure!(nbt_slice.is_empty(), "not all chunk NBT data was read"); Ok(Some(AnvilChunk { data, timestamp })) } } + +struct AnvilChunk { + data: Compound, + timestamp: u32, +} + +/// X and Z positions of a region. +type RegionPos = (i32, i32); + +#[allow(clippy::large_enum_variant)] // We're not moving this around. +#[derive(Debug)] +enum RegionEntry { + /// There is a region file loaded here. + Occupied(Region), + /// There is no region file at this position. Don't try to read it from the + /// filesystem again. + Vacant, +} + +#[derive(Debug)] +struct Region { + file: File, + /// The first 8 KiB in the file. + header: [u8; SECTOR_SIZE * 2], +} + +const SECTOR_SIZE: usize = 4096; + +pub struct AnvilPlugin; + +impl Plugin for AnvilPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_system(remove_unviewed_chunks.in_base_set(CoreSet::PreUpdate)) + .add_systems( + (init_anvil, update_client_views, send_recv_chunks) + .chain() + .in_base_set(CoreSet::PostUpdate) + .before(UpdateClientsSet), + ); + } +} + +fn init_anvil(mut query: Query<(&mut AnvilLevel, &Instance), Added>) { + for (mut level, inst) in &mut query { + if let Some(mut state) = level.worker_state.take() { + state.section_count = inst.section_count(); + thread::spawn(move || anvil_worker(state)); + } + } +} + +/// Removes all chunks no longer viewed by clients. +/// +/// This needs to run in `PreUpdate` where the chunk viewer counts have been +/// updated from the previous tick. +fn remove_unviewed_chunks( + mut instances: Query<(Entity, &mut Instance, &AnvilLevel)>, + mut unload_events: EventWriter, +) { + for (entity, mut inst, anvil) in &mut instances { + inst.retain_chunks(|pos, chunk| { + if chunk.is_viewed_mut() || anvil.ignored_chunks.contains(&pos) { + true + } else { + unload_events.send(ChunkUnloadEvent { + instance: entity, + pos, + }); + false + } + }); + } +} + +fn update_client_views( + clients: Query<(&Location, Ref, View, OldView), With>, + mut instances: Query<(&Instance, &mut AnvilLevel)>, +) { + for (loc, old_loc, view, old_view) in &clients { + let view = view.get(); + let old_view = old_view.get(); + + if loc != &*old_loc || view != old_view || old_loc.is_added() { + let Ok((inst, mut anvil)) = instances.get_mut(loc.0) else { + continue + }; + + let queue_pos = |pos| { + if !anvil.ignored_chunks.contains(&pos) && inst.chunk(pos).is_none() { + // Chunks closer to clients are prioritized. + match anvil.pending.entry(pos) { + Entry::Occupied(mut oe) => { + if let Some(priority) = oe.get_mut() { + let dist = view.pos.distance_squared(pos); + *priority = (*priority).min(dist); + } + } + Entry::Vacant(ve) => { + let dist = view.pos.distance_squared(pos); + ve.insert(Some(dist)); + } + } + } + }; + + // Queue all the new chunks in the view to be sent to the anvil worker. + if old_loc.is_added() { + view.iter().for_each(queue_pos); + } else { + view.diff(old_view).for_each(queue_pos); + } + } + } +} + +fn send_recv_chunks( + mut instances: Query<(Entity, &mut Instance, &mut AnvilLevel)>, + mut to_send: Local>, + mut load_events: EventWriter, +) { + for (entity, mut inst, anvil) in &mut instances { + let anvil = anvil.into_inner(); + + // Insert the chunks that are finished loading into the instance and send load + // events. + for (pos, res) in anvil.receiver.drain() { + anvil.pending.remove(&pos); + + let status = match res { + Ok(Some((chunk, AnvilChunk { data, timestamp }))) => { + inst.insert_chunk(pos, chunk); + ChunkLoadStatus::Success { data, timestamp } + } + Ok(None) => ChunkLoadStatus::Empty, + Err(e) => ChunkLoadStatus::Failed(e), + }; + + load_events.send(ChunkLoadEvent { + instance: entity, + pos, + status, + }); + } + + // Collect all the new chunks that need to be loaded this tick. + for (pos, priority) in &mut anvil.pending { + if let Some(pri) = priority.take() { + to_send.push((pri, *pos)); + } + } + + // Sort chunks by ascending priority. + to_send.sort_unstable_by_key(|(pri, _)| *pri); + + // Send the sorted chunks to be loaded. + for (_, pos) in to_send.drain(..) { + let _ = anvil.sender.try_send(pos); + } + } +} + +fn anvil_worker(mut state: ChunkWorkerState) { + while let Ok(pos) = state.receiver.recv() { + let res = get_chunk(pos, &mut state); + + let _ = state.sender.send((pos, res)); + } + + fn get_chunk(pos: ChunkPos, state: &mut ChunkWorkerState) -> WorkerResult { + let Some(anvil_chunk) = state.get_chunk(pos)? else { + return Ok(None); + }; + + let mut chunk = Chunk::new(state.section_count); + // TODO: account for min_y correctly. + parse_chunk::parse_chunk(&anvil_chunk.data, &mut chunk, 4, |biome| { + state + .biome_to_id + .get(biome.as_str()) + .copied() + .unwrap_or_default() + })?; + + Ok(Some((chunk, anvil_chunk))) + } +} + +/// An event sent by `valence_anvil` after an attempt to load a chunk is made. +#[derive(Debug)] +pub struct ChunkLoadEvent { + /// The [`Instance`] where the chunk is located. + pub instance: Entity, + /// The position of the chunk in the instance. + pub pos: ChunkPos, + pub status: ChunkLoadStatus, +} + +#[derive(Debug)] +pub enum ChunkLoadStatus { + /// A new chunk was successfully loaded and inserted into the instance. + Success { + /// The raw chunk data of the new chunk. + data: Compound, + /// The time this chunk was last modified, measured in seconds since the + /// epoch. + timestamp: u32, + }, + /// The Anvil level does not have a chunk at the position. No chunk was + /// loaded. + Empty, + /// An attempt was made to load the chunk, but something went wrong. + Failed(anyhow::Error), +} + +/// An event sent by `valence_anvil` when a chunk is unloaded from an instance. +#[derive(Debug)] +pub struct ChunkUnloadEvent { + /// The [`Instance`] where the chunk was unloaded. + pub instance: Entity, + /// The position of the chunk that was unloaded. + pub pos: ChunkPos, +} diff --git a/crates/valence_anvil/src/to_valence.rs b/crates/valence_anvil/src/parse_chunk.rs similarity index 79% rename from crates/valence_anvil/src/to_valence.rs rename to crates/valence_anvil/src/parse_chunk.rs index ed4ab17bc..1cdadfaba 100644 --- a/crates/valence_anvil/src/to_valence.rs +++ b/crates/valence_anvil/src/parse_chunk.rs @@ -10,7 +10,7 @@ use valence_nbt::{Compound, List, Value}; #[derive(Clone, Debug, Error)] #[non_exhaustive] -pub enum ToValenceError { +pub(crate) enum ParseChunkError { #[error("missing chunk sections")] MissingSections, #[error("missing chunk section Y")] @@ -45,8 +45,6 @@ pub enum ToValenceError { BadBiomePaletteLen, #[error("biome name is not a valid resource identifier")] BadBiomeName, - #[error("missing biome name")] - MissingBiomeName, #[error("missing packed biome data in section")] MissingBiomeData, #[error("unexpected number of longs in biome data")] @@ -69,27 +67,24 @@ pub enum ToValenceError { /// /// # Arguments /// -/// - `nbt`: The Anvil chunk to read from. This is usually the value returned by -/// [`AnvilWorld::read_chunk`]. +/// - `nbt`: The raw Anvil chunk NBT to read from. /// - `chunk`: The Valence chunk to write to. /// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After /// applying the offset, only the sectors in the range -/// `0..chunk.sector_count()` are written. +/// `0..chunk.section_count()` are written. /// - `map_biome`: A function to map biome resource identifiers in the NBT data /// to Valence [`BiomeId`]s. -/// -/// [`AnvilWorld::read_chunk`]: crate::AnvilWorld::read_chunk -pub fn to_valence( +pub(crate) fn parse_chunk( nbt: &Compound, chunk: &mut Chunk, sect_offset: i32, mut map_biome: F, -) -> Result<(), ToValenceError> +) -> Result<(), ParseChunkError> where F: FnMut(Ident<&str>) -> BiomeId, { let Some(Value::List(List::Compound(sections))) = nbt.get("sections") else { - return Err(ToValenceError::MissingSections) + return Err(ParseChunkError::MissingSections) }; let mut converted_block_palette = vec![]; @@ -97,7 +92,7 @@ where for section in sections { let Some(Value::Byte(sect_y)) = section.get("Y") else { - return Err(ToValenceError::MissingSectionY) + return Err(ParseChunkError::MissingSectionY) }; let adjusted_sect_y = *sect_y as i32 + sect_offset; @@ -108,26 +103,26 @@ where } let Some(Value::Compound(block_states)) = section.get("block_states") else { - return Err(ToValenceError::MissingBlockStates) + return Err(ParseChunkError::MissingBlockStates) }; let Some(Value::List(List::Compound(palette))) = block_states.get("palette") else { - return Err(ToValenceError::MissingBlockPalette) + return Err(ParseChunkError::MissingBlockPalette) }; if !(1..BLOCKS_PER_SECTION).contains(&palette.len()) { - return Err(ToValenceError::BadBlockPaletteLen); + return Err(ParseChunkError::BadBlockPaletteLen); } converted_block_palette.clear(); for block in palette { let Some(Value::String(name)) = block.get("Name") else { - return Err(ToValenceError::MissingBlockName) + return Err(ParseChunkError::MissingBlockName) }; let Some(block_kind) = BlockKind::from_str(ident_path(name)) else { - return Err(ToValenceError::UnknownBlockName(name.into())) + return Err(ParseChunkError::UnknownBlockName(name.into())) }; let mut state = block_kind.to_state(); @@ -135,15 +130,15 @@ where if let Some(Value::Compound(properties)) = block.get("Properties") { for (key, value) in properties { let Value::String(value) = value else { - return Err(ToValenceError::BadPropValueType) + return Err(ParseChunkError::BadPropValueType) }; let Some(prop_name) = PropName::from_str(key) else { - return Err(ToValenceError::UnknownPropName(key.into())) + return Err(ParseChunkError::UnknownPropName(key.into())) }; let Some(prop_value) = PropValue::from_str(value) else { - return Err(ToValenceError::UnknownPropValue(value.into())) + return Err(ParseChunkError::UnknownPropValue(value.into())) }; state = state.set(prop_name, prop_value); @@ -159,7 +154,7 @@ where debug_assert!(converted_block_palette.len() > 1); let Some(Value::LongArray(data)) = block_states.get("data") else { - return Err(ToValenceError::MissingBlockStateData) + return Err(ParseChunkError::MissingBlockStateData) }; let bits_per_idx = bit_width(converted_block_palette.len() - 1).max(4); @@ -168,7 +163,7 @@ where let mask = 2_u64.pow(bits_per_idx as u32) - 1; if long_count != data.len() { - return Err(ToValenceError::BadBlockLongCount); + return Err(ParseChunkError::BadBlockLongCount); }; let mut i = 0; @@ -183,7 +178,7 @@ where let idx = (u64 >> (bits_per_idx * j)) & mask; let Some(block) = converted_block_palette.get(idx as usize).cloned() else { - return Err(ToValenceError::BadBlockPaletteIndex) + return Err(ParseChunkError::BadBlockPaletteIndex) }; let x = i % 16; @@ -198,22 +193,22 @@ where } let Some(Value::Compound(biomes)) = section.get("biomes") else { - return Err(ToValenceError::MissingBiomes) + return Err(ParseChunkError::MissingBiomes) }; let Some(Value::List(List::String(palette))) = biomes.get("palette") else { - return Err(ToValenceError::MissingBiomePalette) + return Err(ParseChunkError::MissingBiomePalette) }; if !(1..BIOMES_PER_SECTION).contains(&palette.len()) { - return Err(ToValenceError::BadBiomePaletteLen); + return Err(ParseChunkError::BadBiomePaletteLen); } converted_biome_palette.clear(); for biome_name in palette { let Ok(ident) = Ident::>::new(biome_name) else { - return Err(ToValenceError::BadBiomeName) + return Err(ParseChunkError::BadBiomeName) }; converted_biome_palette.push(map_biome(ident.as_str_ident())); @@ -225,7 +220,7 @@ where debug_assert!(converted_biome_palette.len() > 1); let Some(Value::LongArray(data)) = biomes.get("data") else { - return Err(ToValenceError::MissingBiomeData) + return Err(ParseChunkError::MissingBiomeData) }; let bits_per_idx = bit_width(converted_biome_palette.len() - 1); @@ -234,7 +229,7 @@ where let mask = 2_u64.pow(bits_per_idx as u32) - 1; if long_count != data.len() { - return Err(ToValenceError::BadBiomeLongCount); + return Err(ParseChunkError::BadBiomeLongCount); }; let mut i = 0; @@ -249,7 +244,7 @@ where let idx = (u64 >> (bits_per_idx * j)) & mask; let Some(biome) = converted_biome_palette.get(idx as usize).cloned() else { - return Err(ToValenceError::BadBiomePaletteIndex) + return Err(ParseChunkError::BadBiomePaletteIndex) }; let x = i % 4; @@ -265,41 +260,41 @@ where } let Some(Value::List(block_entities)) = nbt.get("block_entities") else { - return Err(ToValenceError::MissingBlockEntity); + return Err(ParseChunkError::MissingBlockEntity); }; if let List::Compound(block_entities) = block_entities { for comp in block_entities { let Some(Value::String(ident)) = comp.get("id") else { - return Err(ToValenceError::MissingBlockEntityIdent); + return Err(ParseChunkError::MissingBlockEntityIdent); }; let Ok(ident) = Ident::new(&ident[..]) else { - return Err(ToValenceError::UnknownBlockEntityIdent(ident.clone())); + return Err(ParseChunkError::UnknownBlockEntityIdent(ident.clone())); }; let Some(kind) = BlockEntityKind::from_ident(ident.as_str_ident()) else { - return Err(ToValenceError::UnknownBlockEntityIdent(ident.as_str().to_string())); + return Err(ParseChunkError::UnknownBlockEntityIdent(ident.as_str().to_string())); }; let block_entity = BlockEntity { kind, nbt: comp.clone(), }; let Some(Value::Int(x)) = comp.get("x") else { - return Err(ToValenceError::InvalidBlockEntityPosition); + return Err(ParseChunkError::InvalidBlockEntityPosition); }; let Ok(x) = usize::try_from(x.mod_floor(&16)) else { - return Err(ToValenceError::InvalidBlockEntityPosition); + return Err(ParseChunkError::InvalidBlockEntityPosition); }; let Some(Value::Int(y)) = comp.get("y") else { - return Err(ToValenceError::InvalidBlockEntityPosition); + return Err(ParseChunkError::InvalidBlockEntityPosition); }; let Ok(y) = usize::try_from(y + sect_offset * 16) else { - return Err(ToValenceError::InvalidBlockEntityPosition); + return Err(ParseChunkError::InvalidBlockEntityPosition); }; let Some(Value::Int(z)) = comp.get("z") else { - return Err(ToValenceError::InvalidBlockEntityPosition); + return Err(ParseChunkError::InvalidBlockEntityPosition); }; let Ok(z) = usize::try_from(z.mod_floor(&16)) else { - return Err(ToValenceError::InvalidBlockEntityPosition); + return Err(ParseChunkError::InvalidBlockEntityPosition); }; chunk.set_block_entity(x, y, z, block_entity); diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index c06f03679..55c6d9b66 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -72,7 +72,6 @@ use valence_registry::tags::TagsRegistry; use valence_registry::RegistrySet; pub mod action; -pub mod chat; pub mod command; pub mod custom_payload; pub mod event_loop; @@ -81,6 +80,7 @@ pub mod interact_block; pub mod interact_entity; pub mod interact_item; pub mod keepalive; +pub mod message; pub mod movement; pub mod op_level; pub mod packet; @@ -107,8 +107,10 @@ pub struct FlushPacketsSet; pub struct SpawnClientsSet; +/// The system set where various facets of the client are updated. Systems that +/// modify chunks should run _before_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] -struct UpdateClientsSet; +pub struct UpdateClientsSet; impl Plugin for ClientPlugin { fn build(&self, app: &mut App) { @@ -150,7 +152,7 @@ impl Plugin for ClientPlugin { action::build(app); teleport::build(app); weather::build(app); - chat::build(app); + message::build(app); custom_payload::build(app); hand_swing::build(app); interact_block::build(app); diff --git a/crates/valence_client/src/chat.rs b/crates/valence_client/src/message.rs similarity index 68% rename from crates/valence_client/src/chat.rs rename to crates/valence_client/src/message.rs index 4be42ae2a..906bac8ce 100644 --- a/crates/valence_client/src/chat.rs +++ b/crates/valence_client/src/message.rs @@ -7,7 +7,6 @@ use valence_core::protocol::packet::chat::{ChatMessageC2s, GameMessageS2c}; use valence_core::text::Text; use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent}; -use crate::Client; pub(super) fn build(app: &mut App) { app.add_event::().add_system( @@ -17,22 +16,34 @@ pub(super) fn build(app: &mut App) { ); } -#[derive(Clone, Debug)] -pub struct ChatMessageEvent { - pub client: Entity, - pub message: Box, - pub timestamp: u64, +pub trait SendMessage { + /// Sends a system message visible in the chat. + fn send_chat_message(&mut self, msg: impl Into); + /// Displays a message in the player's action bar (text above the hotbar). + fn send_action_bar_message(&mut self, msg: impl Into); } -impl Client { - /// Sends a system message to the player which is visible in the chat. The - /// message is only visible to this client. - pub fn send_message(&mut self, msg: impl Into) { +impl SendMessage for T { + fn send_chat_message(&mut self, msg: impl Into) { self.write_packet(&GameMessageS2c { chat: msg.into().into(), overlay: false, }); } + + fn send_action_bar_message(&mut self, msg: impl Into) { + self.write_packet(&GameMessageS2c { + chat: msg.into().into(), + overlay: true, + }); + } +} + +#[derive(Clone, Debug)] +pub struct ChatMessageEvent { + pub client: Entity, + pub message: Box, + pub timestamp: u64, } pub fn handle_chat_message( diff --git a/crates/valence_instance/src/lib.rs b/crates/valence_instance/src/lib.rs index 862e31eb6..4d4301061 100644 --- a/crates/valence_instance/src/lib.rs +++ b/crates/valence_instance/src/lib.rs @@ -64,7 +64,8 @@ mod paletted_container; pub struct InstancePlugin; /// When Minecraft entity changes are written to the packet buffers of chunks. -/// Systems that read from the packet buffer of chunks should run _after_ this. +/// Systems that modify entites should run _before_ this. Systems that read from +/// the packet buffer of chunks should run _after_ this. #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct WriteUpdatePacketsToInstancesSet; diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 2c4cce173..59b23d1e0 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -45,7 +45,8 @@ //! ## Access other world border properties. //! Access to the rest of the world border properties is fairly straightforward //! by querying their respective component. [`WorldBorderBundle`] contains -//! references for all properties of the world border and their respective component +//! references for all properties of the world border and their respective +//! component #![allow(clippy::type_complexity)] #![deny( rustdoc::broken_intra_doc_links,