mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 06:21:31 +11:00
Anvil Rework (#367)
## Description Solidify the design of `valence_anvil` so that most of the boilerplate in the anvil example is eliminated. `AnvilLevel` is now a component of `Instance` and automatically loads and unloads chunks as clients move around. Events are used to communicate when chunks are loaded and unloaded. Also changes the system message API and introduces the `SendMessage` trait. Checks off a box in #288 ### Known Issues - Still no support for saving or entities. - The handling of chunk `min_y` is wrong. I plan to fix this in an upcoming redesign of instances and chunks. - Uses one OS thread per anvil level. This could be improved with a dedicated shared thread pool to parallelize the loading process. However, it seems decently fast as it is. - Old benchmark is commented out. - Could use some tests.
This commit is contained in:
parent
61f2279831
commit
2ed5a8840d
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -10,7 +10,7 @@ mod var_long;
|
|||
|
||||
criterion_group! {
|
||||
benches,
|
||||
anvil::load,
|
||||
// anvil::load,
|
||||
block::block,
|
||||
decode_array::decode_array,
|
||||
idle::idle_update,
|
||||
|
|
|
@ -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<ChunkPos, Option<Priority>>,
|
||||
sender: Sender<ChunkPos>,
|
||||
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<DimensionTypeRegistry>,
|
||||
biomes: Res<BiomeRegistry>,
|
||||
server: Res<Server>,
|
||||
cli: Res<Cli>,
|
||||
) {
|
||||
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<GameState>,
|
||||
fn handle_chunk_loads(
|
||||
mut events: EventReader<ChunkLoadEvent>,
|
||||
mut instances: Query<&mut Instance, With<AnvilLevel>>,
|
||||
) {
|
||||
let instance = instances.single_mut();
|
||||
let mut inst = 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));
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut<GameState>) {
|
||||
let mut instance = instances.single_mut();
|
||||
let state = state.into_inner();
|
||||
// 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<usize>) {
|
||||
let mut inst = instances.single_mut();
|
||||
|
||||
// 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());
|
||||
}
|
||||
let cnt = inst.chunks().count();
|
||||
|
||||
// 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);
|
||||
if *last_count != cnt {
|
||||
*last_count = cnt;
|
||||
inst.send_action_bar_message(
|
||||
"Chunk Count: ".into_text() + (cnt as i32).color(Color::LIGHT_PURPLE),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn anvil_worker(
|
||||
receiver: Receiver<ChunkPos>,
|
||||
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));
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed to get chunk at ({}, {}): {e:#}.", pos.x, pos.z),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_chunk(pos: ChunkPos, world: &mut AnvilWorld) -> anyhow::Result<Option<Chunk>> {
|
||||
let Some(AnvilChunk { data, .. }) = world.read_chunk(pos.x, pos.z)? else {
|
||||
return Ok(None)
|
||||
};
|
||||
|
||||
let mut chunk = Chunk::new(SECTION_COUNT);
|
||||
|
||||
valence_anvil::to_valence(&data, &mut chunk, 4, |_| BiomeId::default())?;
|
||||
|
||||
Ok(Some(chunk))
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Entity, With<Instance>>,
|
||||
) {
|
||||
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(),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ impl PluginGroup for DefaultPlugins {
|
|||
|
||||
#[cfg(feature = "anvil")]
|
||||
{
|
||||
// No plugin... yet.
|
||||
group = group.add(valence_anvil::AnvilPlugin);
|
||||
}
|
||||
|
||||
#[cfg(feature = "advancement")]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
mod parse_chunk;
|
||||
|
||||
#[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(Component, Debug)]
|
||||
pub struct AnvilLevel {
|
||||
/// Chunk worker state to be moved to another thread.
|
||||
worker_state: Option<ChunkWorkerState>,
|
||||
/// 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<ChunkPos>,
|
||||
/// Chunks that need to be loaded. Chunks with `None` priority have already
|
||||
/// been sent to the anvil thread.
|
||||
pending: HashMap<ChunkPos, Option<Priority>>,
|
||||
/// Sender for the chunk worker thread.
|
||||
sender: Sender<ChunkPos>,
|
||||
/// Receiver for the chunk worker thread.
|
||||
receiver: Receiver<(ChunkPos, WorkerResult)>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
type WorkerResult = anyhow::Result<Option<(Chunk, AnvilChunk)>>;
|
||||
|
||||
#[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],
|
||||
}
|
||||
|
||||
const SECTOR_SIZE: usize = 4096;
|
||||
|
||||
impl AnvilWorld {
|
||||
pub fn new(world_root: impl Into<PathBuf>) -> Self {
|
||||
impl AnvilLevel {
|
||||
pub fn new(world_root: impl Into<PathBuf>, 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<Option<AnvilChunk>, 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add tombstone for missing region file in `regions`.
|
||||
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<RegionPos, RegionEntry>,
|
||||
/// 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<ChunkPos>,
|
||||
/// Scratch buffer for decompression.
|
||||
decompress_buf: Vec<u8>,
|
||||
/// Mapping of biome names to their biome ID.
|
||||
biome_to_id: BTreeMap<Ident<String>, BiomeId>,
|
||||
/// Number of chunk sections in the instance.
|
||||
section_count: usize,
|
||||
}
|
||||
|
||||
impl ChunkWorkerState {
|
||||
fn get_chunk(&mut self, pos: ChunkPos) -> anyhow::Result<Option<AnvilChunk>> {
|
||||
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::<BigEndian>()?;
|
||||
let timestamp = (®ion.header[chunk_idx * 4 + SECTOR_SIZE..]).read_u32::<BigEndian>()?;
|
||||
|
@ -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::<BigEndian>()? 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::<ChunkLoadEvent>()
|
||||
.add_event::<ChunkUnloadEvent>()
|
||||
.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<AnvilLevel>>) {
|
||||
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<ChunkUnloadEvent>,
|
||||
) {
|
||||
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<OldLocation>, View, OldView), With<Client>>,
|
||||
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<Vec<(Priority, ChunkPos)>>,
|
||||
mut load_events: EventWriter<ChunkLoadEvent>,
|
||||
) {
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -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<F, const LOADED: bool>(
|
||||
pub(crate) fn parse_chunk<F, const LOADED: bool>(
|
||||
nbt: &Compound,
|
||||
chunk: &mut Chunk<LOADED>,
|
||||
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::<Cow<str>>::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);
|
|
@ -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);
|
||||
|
|
|
@ -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::<ChatMessageEvent>().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<str>,
|
||||
pub timestamp: u64,
|
||||
pub trait SendMessage {
|
||||
/// Sends a system message visible in the chat.
|
||||
fn send_chat_message(&mut self, msg: impl Into<Text>);
|
||||
/// Displays a message in the player's action bar (text above the hotbar).
|
||||
fn send_action_bar_message(&mut self, msg: impl Into<Text>);
|
||||
}
|
||||
|
||||
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<Text>) {
|
||||
impl<T: WritePacket> SendMessage for T {
|
||||
fn send_chat_message(&mut self, msg: impl Into<Text>) {
|
||||
self.write_packet(&GameMessageS2c {
|
||||
chat: msg.into().into(),
|
||||
overlay: false,
|
||||
});
|
||||
}
|
||||
|
||||
fn send_action_bar_message(&mut self, msg: impl Into<Text>) {
|
||||
self.write_packet(&GameMessageS2c {
|
||||
chat: msg.into().into(),
|
||||
overlay: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChatMessageEvent {
|
||||
pub client: Entity,
|
||||
pub message: Box<str>,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
pub fn handle_chat_message(
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue