mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +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"
|
heck = "0.4.0"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
indexmap = "1.9.3"
|
indexmap = "1.9.3"
|
||||||
|
lru = "0.10.0"
|
||||||
noise = "0.8.2"
|
noise = "0.8.2"
|
||||||
num = "0.4.0"
|
num = "0.4.0"
|
||||||
num-bigint = "0.4.3"
|
num-bigint = "0.4.3"
|
||||||
|
|
|
@ -10,7 +10,7 @@ Ignoring transitive dependencies and `valence_core`, the dependency graph can be
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
network --> client
|
network --> client
|
||||||
client --> instance
|
client --> instance
|
||||||
biome --> registry
|
biome --> registry
|
||||||
dimension --> registry
|
dimension --> registry
|
||||||
|
@ -19,7 +19,7 @@ graph TD
|
||||||
instance --> entity
|
instance --> entity
|
||||||
player_list --> client
|
player_list --> client
|
||||||
inventory --> client
|
inventory --> client
|
||||||
anvil --> instance
|
anvil --> client
|
||||||
entity --> block
|
entity --> block
|
||||||
advancement --> client
|
advancement --> client
|
||||||
world_border --> client
|
world_border --> client
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/*
|
||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
use std::hint::black_box;
|
use std::hint::black_box;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -133,3 +134,4 @@ fn get_world_asset(
|
||||||
|
|
||||||
Ok(final_path)
|
Ok(final_path)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -10,7 +10,7 @@ mod var_long;
|
||||||
|
|
||||||
criterion_group! {
|
criterion_group! {
|
||||||
benches,
|
benches,
|
||||||
anvil::load,
|
// anvil::load,
|
||||||
block::block,
|
block::block,
|
||||||
decode_array::decode_array,
|
decode_array::decode_array,
|
||||||
idle::idle_update,
|
idle::idle_update,
|
||||||
|
|
|
@ -1,80 +1,44 @@
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use flume::{Receiver, Sender};
|
|
||||||
use tracing::warn;
|
|
||||||
use valence::anvil::{AnvilChunk, AnvilWorld};
|
|
||||||
use valence::prelude::*;
|
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 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)]
|
#[clap(author, version, about)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// The path to a Minecraft world save containing a `region` subdirectory.
|
/// The path to a Minecraft world save containing a `region` subdirectory.
|
||||||
path: PathBuf,
|
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() {
|
pub fn main() {
|
||||||
tracing_subscriber::fmt().init();
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let dir = cli.path;
|
|
||||||
|
|
||||||
if !dir.exists() {
|
if !cli.path.exists() {
|
||||||
eprintln!("Directory `{}` does not exist. Exiting.", dir.display());
|
eprintln!(
|
||||||
return;
|
"Directory `{}` does not exist. Exiting.",
|
||||||
} else if !dir.is_dir() {
|
cli.path.display()
|
||||||
eprintln!("`{}` is not a directory. Exiting.", dir.display());
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let anvil = AnvilWorld::new(dir);
|
if !cli.path.is_dir() {
|
||||||
|
eprintln!("`{}` is not a directory. Exiting.", cli.path.display());
|
||||||
let (finished_sender, finished_receiver) = flume::unbounded();
|
return;
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
.insert_resource(game_state)
|
.insert_resource(cli)
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_systems(
|
|
||||||
(
|
|
||||||
init_clients,
|
|
||||||
remove_unviewed_chunks,
|
|
||||||
update_client_views,
|
|
||||||
send_recv_chunks,
|
|
||||||
)
|
|
||||||
.chain(),
|
|
||||||
)
|
|
||||||
.add_system(despawn_disconnected_clients)
|
.add_system(despawn_disconnected_clients)
|
||||||
|
.add_systems((init_clients, handle_chunk_loads).chain())
|
||||||
|
.add_system(display_loaded_chunk_count)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,9 +47,24 @@ fn setup(
|
||||||
dimensions: Res<DimensionTypeRegistry>,
|
dimensions: Res<DimensionTypeRegistry>,
|
||||||
biomes: Res<BiomeRegistry>,
|
biomes: Res<BiomeRegistry>,
|
||||||
server: Res<Server>,
|
server: Res<Server>,
|
||||||
|
cli: Res<Cli>,
|
||||||
) {
|
) {
|
||||||
let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server);
|
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(
|
fn init_clients(
|
||||||
|
@ -100,103 +79,44 @@ fn init_clients(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) {
|
fn handle_chunk_loads(
|
||||||
instances
|
mut events: EventReader<ChunkLoadEvent>,
|
||||||
.single_mut()
|
mut instances: Query<&mut Instance, With<AnvilLevel>>,
|
||||||
.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>,
|
|
||||||
) {
|
) {
|
||||||
let instance = instances.single_mut();
|
let mut inst = instances.single_mut();
|
||||||
|
|
||||||
for (client, view, old_view) in &mut clients {
|
for event in events.iter() {
|
||||||
let view = view.get();
|
match &event.status {
|
||||||
let queue_pos = |pos| {
|
ChunkLoadStatus::Success { .. } => {
|
||||||
if instance.chunk(pos).is_none() {
|
// The chunk was inserted into the world. Nothing for us to do.
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
ChunkLoadStatus::Empty => {
|
||||||
|
// There's no chunk here so let's insert an empty chunk. If we were doing
|
||||||
// Queue all the new chunks in the view to be sent to the anvil worker.
|
// terrain generation we would prepare that here.
|
||||||
if client.is_added() {
|
inst.insert_chunk(event.pos, Chunk::default());
|
||||||
view.iter().for_each(queue_pos);
|
}
|
||||||
} else {
|
ChunkLoadStatus::Failed(e) => {
|
||||||
let old_view = old_view.get();
|
// Something went wrong.
|
||||||
if old_view != view {
|
eprintln!(
|
||||||
view.diff(old_view).for_each(queue_pos);
|
"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>) {
|
// Display the number of loaded chunks in the action bar of all clients.
|
||||||
let mut instance = instances.single_mut();
|
fn display_loaded_chunk_count(mut instances: Query<&mut Instance>, mut last_count: Local<usize>) {
|
||||||
let state = state.into_inner();
|
let mut inst = instances.single_mut();
|
||||||
|
|
||||||
// Insert the chunks that are finished loading into the instance.
|
let cnt = inst.chunks().count();
|
||||||
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.
|
if *last_count != cnt {
|
||||||
let mut to_send = vec![];
|
*last_count = cnt;
|
||||||
|
inst.send_action_bar_message(
|
||||||
for (pos, priority) in &mut state.pending {
|
"Chunk Count: ".into_text() + (cnt as i32).color(Color::LIGHT_PURPLE),
|
||||||
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<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::nbt::{compound, List};
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
use valence_client::chat::ChatMessageEvent;
|
|
||||||
use valence_client::interact_block::InteractBlockEvent;
|
use valence_client::interact_block::InteractBlockEvent;
|
||||||
|
use valence_client::message::ChatMessageEvent;
|
||||||
|
|
||||||
const FLOOR_Y: i32 = 64;
|
const FLOOR_Y: i32 = 64;
|
||||||
const SIGN_POS: [i32; 3] = [3, FLOOR_Y + 1, 2];
|
const SIGN_POS: [i32; 3] = [3, FLOOR_Y + 1, 2];
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use valence::inventory::HeldItem;
|
use valence::inventory::HeldItem;
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
use valence_client::interact_block::InteractBlockEvent;
|
use valence_client::interact_block::InteractBlockEvent;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ fn init_clients(
|
||||||
loc.0 = instances.single();
|
loc.0 = instances.single();
|
||||||
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
|
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 std::mem;
|
||||||
|
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
|
|
||||||
const BOARD_MIN_X: i32 = -30;
|
const BOARD_MIN_X: i32 = -30;
|
||||||
const BOARD_MAX_X: i32 = 30;
|
const BOARD_MAX_X: i32 = 30;
|
||||||
|
@ -74,8 +75,8 @@ fn init_clients(
|
||||||
instances: Query<Entity, With<Instance>>,
|
instances: Query<Entity, With<Instance>>,
|
||||||
) {
|
) {
|
||||||
for (mut client, mut loc, mut pos) in &mut clients {
|
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_chat_message("Welcome to Conway's game of life in Minecraft!".italic());
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
"Sneak to toggle running the simulation and the left mouse button to bring blocks to \
|
"Sneak to toggle running the simulation and the left mouse button to bring blocks to \
|
||||||
life."
|
life."
|
||||||
.italic(),
|
.italic(),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
use valence_client::status::RequestRespawnEvent;
|
use valence_client::status::RequestRespawnEvent;
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
@ -58,7 +59,7 @@ fn init_clients(
|
||||||
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
|
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
|
||||||
has_respawn_screen.0 = true;
|
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(),
|
"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 bevy_ecs::prelude::Entity;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
use valence_entity::entity::NameVisible;
|
use valence_entity::entity::NameVisible;
|
||||||
use valence_entity::hoglin::HoglinEntityBundle;
|
use valence_entity::hoglin::HoglinEntityBundle;
|
||||||
use valence_entity::pig::PigEntityBundle;
|
use valence_entity::pig::PigEntityBundle;
|
||||||
|
@ -53,7 +54,7 @@ fn init_clients(
|
||||||
loc.0 = instances.single();
|
loc.0 = instances.single();
|
||||||
pos.set([0.5, 65.0, 0.5]);
|
pos.set([0.5, 65.0, 0.5]);
|
||||||
*game_mode = GameMode::Creative;
|
*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 rand::Rng;
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
use valence::protocol::packet::sound::{Sound, SoundCategory};
|
use valence::protocol::packet::sound::{Sound, SoundCategory};
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
|
|
||||||
const START_POS: BlockPos = BlockPos::new(0, 100, 0);
|
const START_POS: BlockPos = BlockPos::new(0, 100, 0);
|
||||||
const VIEW_DIST: u8 = 10;
|
const VIEW_DIST: u8 = 10;
|
||||||
|
@ -66,7 +67,7 @@ fn init_clients(
|
||||||
is_flat.0 = true;
|
is_flat.0 = true;
|
||||||
*game_mode = GameMode::Adventure;
|
*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 {
|
let state = GameState {
|
||||||
blocks: VecDeque::new(),
|
blocks: VecDeque::new(),
|
||||||
|
@ -96,7 +97,7 @@ fn reset_clients(
|
||||||
|
|
||||||
if out_of_bounds || state.is_added() {
|
if out_of_bounds || state.is_added() {
|
||||||
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()
|
"Your score was ".italic()
|
||||||
+ state
|
+ state
|
||||||
.score
|
.score
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use valence::player_list::{DisplayName, PlayerListEntryBundle};
|
use valence::player_list::{DisplayName, PlayerListEntryBundle};
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
use valence_client::Ping;
|
use valence_client::Ping;
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
@ -62,7 +63,7 @@ fn init_clients(
|
||||||
loc.0 = instances.single();
|
loc.0 = instances.single();
|
||||||
*game_mode = GameMode::Creative;
|
*game_mode = GameMode::Creative;
|
||||||
|
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
"Please open your player list (tab key)."
|
"Please open your player list (tab key)."
|
||||||
.italic()
|
.italic()
|
||||||
.color(Color::WHITE),
|
.color(Color::WHITE),
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use valence::entity::player::PlayerEntityBundle;
|
use valence::entity::player::PlayerEntityBundle;
|
||||||
use valence::entity::sheep::SheepEntityBundle;
|
use valence::entity::sheep::SheepEntityBundle;
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
use valence_client::resource_pack::{ResourcePackStatus, ResourcePackStatusEvent};
|
use valence_client::resource_pack::{ResourcePackStatus, ResourcePackStatusEvent};
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
@ -57,7 +58,7 @@ fn init_clients(
|
||||||
for (entity, uuid, mut client, mut game_mode) in &mut clients {
|
for (entity, uuid, mut client, mut game_mode) in &mut clients {
|
||||||
*game_mode = GameMode::Creative;
|
*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 {
|
commands.entity(entity).insert(PlayerEntityBundle {
|
||||||
location: Location(instances.single()),
|
location: Location(instances.single()),
|
||||||
|
@ -91,17 +92,18 @@ fn on_resource_pack_status(
|
||||||
if let Ok(mut client) = clients.get_mut(event.client) {
|
if let Ok(mut client) = clients.get_mut(event.client) {
|
||||||
match event.status {
|
match event.status {
|
||||||
ResourcePackStatus::Accepted => {
|
ResourcePackStatus::Accepted => {
|
||||||
client.send_message("Resource pack accepted.".color(Color::GREEN));
|
client.send_chat_message("Resource pack accepted.".color(Color::GREEN));
|
||||||
}
|
}
|
||||||
ResourcePackStatus::Declined => {
|
ResourcePackStatus::Declined => {
|
||||||
client.send_message("Resource pack declined.".color(Color::RED));
|
client.send_chat_message("Resource pack declined.".color(Color::RED));
|
||||||
}
|
}
|
||||||
ResourcePackStatus::FailedDownload => {
|
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 => {
|
ResourcePackStatus::SuccessfullyLoaded => {
|
||||||
client
|
client.send_chat_message(
|
||||||
.send_message("Resource pack successfully downloaded.".color(Color::BLUE));
|
"Resource pack successfully downloaded.".color(Color::BLUE),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
|
||||||
|
@ -47,16 +48,17 @@ fn init_clients(
|
||||||
loc.0 = instances.single();
|
loc.0 = instances.single();
|
||||||
*game_mode = GameMode::Creative;
|
*game_mode = GameMode::Creative;
|
||||||
|
|
||||||
client.send_message("Welcome to the text example.".bold());
|
client.send_chat_message("Welcome to the text example.".bold());
|
||||||
client
|
client.send_chat_message(
|
||||||
.send_message("The following examples show ways to use the different text components.");
|
"The following examples show ways to use the different text components.",
|
||||||
|
);
|
||||||
|
|
||||||
// Text examples
|
// Text examples
|
||||||
client.send_message("\nText");
|
client.send_chat_message("\nText");
|
||||||
client.send_message(" - ".into_text() + Text::text("Plain text"));
|
client.send_chat_message(" - ".into_text() + Text::text("Plain text"));
|
||||||
client.send_message(" - ".into_text() + Text::text("Styled text").italic());
|
client.send_chat_message(" - ".into_text() + Text::text("Styled text").italic());
|
||||||
client.send_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD));
|
client.send_chat_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD));
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - ".into_text()
|
" - ".into_text()
|
||||||
+ Text::text("Colored and styled text")
|
+ Text::text("Colored and styled text")
|
||||||
.color(Color::GOLD)
|
.color(Color::GOLD)
|
||||||
|
@ -65,59 +67,61 @@ fn init_clients(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Translated text examples
|
// Translated text examples
|
||||||
client.send_message("\nTranslated Text");
|
client.send_chat_message("\nTranslated Text");
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - 'chat.type.advancement.task': ".into_text()
|
" - 'chat.type.advancement.task': ".into_text()
|
||||||
+ Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []),
|
+ Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []),
|
||||||
);
|
);
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - 'chat.type.advancement.task' with slots: ".into_text()
|
" - 'chat.type.advancement.task' with slots: ".into_text()
|
||||||
+ Text::translate(
|
+ Text::translate(
|
||||||
translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
|
translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
|
||||||
["arg1".into(), "arg2".into()],
|
["arg1".into(), "arg2".into()],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - 'custom.translation_key': ".into_text()
|
" - 'custom.translation_key': ".into_text()
|
||||||
+ Text::translate("custom.translation_key", []),
|
+ Text::translate("custom.translation_key", []),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Scoreboard value example
|
// Scoreboard value example
|
||||||
client.send_message("\nScoreboard Values");
|
client.send_chat_message("\nScoreboard Values");
|
||||||
client.send_message(" - Score: ".into_text() + Text::score("*", "objective", None));
|
client.send_chat_message(" - Score: ".into_text() + Text::score("*", "objective", None));
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - Score with custom value: ".into_text()
|
" - Score with custom value: ".into_text()
|
||||||
+ Text::score("*", "objective", Some("value".into())),
|
+ Text::score("*", "objective", Some("value".into())),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Entity names example
|
// Entity names example
|
||||||
client.send_message("\nEntity Names (Selector)");
|
client.send_chat_message("\nEntity Names (Selector)");
|
||||||
client.send_message(" - Nearest player: ".into_text() + Text::selector("@p", None));
|
client.send_chat_message(" - Nearest player: ".into_text() + Text::selector("@p", None));
|
||||||
client.send_message(" - Random player: ".into_text() + Text::selector("@r", None));
|
client.send_chat_message(" - Random player: ".into_text() + Text::selector("@r", None));
|
||||||
client.send_message(" - All players: ".into_text() + Text::selector("@a", None));
|
client.send_chat_message(" - All players: ".into_text() + Text::selector("@a", None));
|
||||||
client.send_message(" - All entities: ".into_text() + Text::selector("@e", None));
|
client.send_chat_message(" - All entities: ".into_text() + Text::selector("@e", None));
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - All entities with custom separator: ".into_text()
|
" - All entities with custom separator: ".into_text()
|
||||||
+ Text::selector("@e", Some(", ".into_text().color(Color::GOLD))),
|
+ Text::selector("@e", Some(", ".into_text().color(Color::GOLD))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keybind example
|
// Keybind example
|
||||||
client.send_message("\nKeybind");
|
client.send_chat_message("\nKeybind");
|
||||||
client.send_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory"));
|
client
|
||||||
|
.send_chat_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory"));
|
||||||
|
|
||||||
// NBT examples
|
// NBT examples
|
||||||
client.send_message("\nNBT");
|
client.send_chat_message("\nNBT");
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
" - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None),
|
" - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None),
|
||||||
);
|
);
|
||||||
client
|
client.send_chat_message(
|
||||||
.send_message(" - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None));
|
" - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None),
|
||||||
client.send_message(
|
);
|
||||||
|
client.send_chat_message(
|
||||||
" - Storage NBT: ".into_text()
|
" - Storage NBT: ".into_text()
|
||||||
+ Text::storage_nbt(ident!("storage.key"), "@a", None, None),
|
+ Text::storage_nbt(ident!("storage.key"), "@a", None, None),
|
||||||
);
|
);
|
||||||
|
|
||||||
client.send_message(
|
client.send_chat_message(
|
||||||
"\n\n↑ ".into_text().bold().color(Color::GOLD)
|
"\n\n↑ ".into_text().bold().color(Color::GOLD)
|
||||||
+ "Scroll up to see the full example!".into_text().not_bold(),
|
+ "Scroll up to see the full example!".into_text().not_bold(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use bevy_app::App;
|
use bevy_app::App;
|
||||||
use valence::client::chat::ChatMessageEvent;
|
|
||||||
use valence::client::despawn_disconnected_clients;
|
use valence::client::despawn_disconnected_clients;
|
||||||
|
use valence::client::message::ChatMessageEvent;
|
||||||
use valence::inventory::HeldItem;
|
use valence::inventory::HeldItem;
|
||||||
use valence::prelude::*;
|
use valence::prelude::*;
|
||||||
use valence::world_border::*;
|
use valence::world_border::*;
|
||||||
|
use valence_client::message::SendMessage;
|
||||||
|
|
||||||
const SPAWN_Y: i32 = 64;
|
const SPAWN_Y: i32 = 64;
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ fn init_clients(
|
||||||
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
|
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
|
||||||
let pickaxe = Some(ItemStack::new(ItemKind::WoodenPickaxe, 1, None));
|
let pickaxe = Some(ItemStack::new(ItemKind::WoodenPickaxe, 1, None));
|
||||||
inv.set_slot(main_slot.slot(), pickaxe);
|
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")]
|
#[cfg(feature = "anvil")]
|
||||||
{
|
{
|
||||||
// No plugin... yet.
|
group = group.add(valence_anvil::AnvilPlugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "advancement")]
|
#[cfg(feature = "advancement")]
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "valence_anvil"
|
name = "valence_anvil"
|
||||||
description = "A library for Minecraft's Anvil world format."
|
description = "Anvil world format support for Valence"
|
||||||
documentation.workspace = true
|
documentation.workspace = true
|
||||||
repository = "https://github.com/valence-rs/valence/tree/main/crates/valence_anvil"
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
keywords = ["anvil", "minecraft", "deserialization"]
|
keywords = ["anvil", "minecraft", "deserialization"]
|
||||||
|
@ -10,12 +9,20 @@ version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
bevy_app.workspace = true
|
||||||
|
bevy_ecs.workspace = true
|
||||||
byteorder.workspace = true
|
byteorder.workspace = true
|
||||||
flate2.workspace = true
|
flate2.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
|
lru.workspace = true
|
||||||
num-integer.workspace = true
|
num-integer.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
valence_biome.workspace = true
|
valence_biome.workspace = true
|
||||||
valence_block.workspace = true
|
valence_block.workspace = true
|
||||||
|
valence_client.workspace = true
|
||||||
valence_core.workspace = true
|
valence_core.workspace = true
|
||||||
|
valence_entity.workspace = true
|
||||||
valence_instance.workspace = true
|
valence_instance.workspace = true
|
||||||
valence_nbt.workspace = true
|
valence_nbt.workspace = true
|
||||||
|
|
|
@ -17,99 +17,149 @@
|
||||||
clippy::dbg_macro
|
clippy::dbg_macro
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use std::collections::btree_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
|
||||||
use std::io::{ErrorKind, Read, Seek, SeekFrom};
|
use std::io::{ErrorKind, Read, Seek, SeekFrom};
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use anyhow::{bail, ensure};
|
||||||
|
use bevy_app::prelude::*;
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
use byteorder::{BigEndian, ReadBytesExt};
|
use byteorder::{BigEndian, ReadBytesExt};
|
||||||
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
use flate2::bufread::{GzDecoder, ZlibDecoder};
|
||||||
use thiserror::Error;
|
use flume::{Receiver, Sender};
|
||||||
pub use to_valence::*;
|
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;
|
use valence_nbt::Compound;
|
||||||
|
|
||||||
mod to_valence;
|
mod parse_chunk;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct AnvilWorld {
|
pub struct AnvilLevel {
|
||||||
/// Path to the "region" subdirectory in the world root.
|
/// Chunk worker state to be moved to another thread.
|
||||||
region_root: PathBuf,
|
worker_state: Option<ChunkWorkerState>,
|
||||||
// TODO: LRU cache for region file handles.
|
/// The set of chunk positions that should not be loaded or unloaded by
|
||||||
/// Maps region (x, z) positions to region files.
|
/// the anvil system.
|
||||||
regions: BTreeMap<(i32, i32), Region>,
|
///
|
||||||
|
/// 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)]
|
type WorkerResult = anyhow::Result<Option<(Chunk, AnvilChunk)>>;
|
||||||
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)]
|
impl AnvilLevel {
|
||||||
#[non_exhaustive]
|
pub fn new(world_root: impl Into<PathBuf>, biomes: &BiomeRegistry) -> Self {
|
||||||
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 {
|
|
||||||
let mut region_root = world_root.into();
|
let mut region_root = world_root.into();
|
||||||
region_root.push("region");
|
region_root.push("region");
|
||||||
|
|
||||||
|
let (pending_sender, pending_receiver) = flume::unbounded();
|
||||||
|
let (finished_sender, finished_receiver) = flume::bounded(4096);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
region_root,
|
worker_state: Some(ChunkWorkerState {
|
||||||
regions: BTreeMap::new(),
|
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
|
/// Forces a chunk to be loaded at a specific position in this world. This
|
||||||
/// no chunk exists at the position, then `None` is returned.
|
/// will bypass [`AnvilLevel::ignored_chunks`].
|
||||||
pub fn read_chunk(
|
/// Note that the chunk will be unloaded next tick unless it has been added
|
||||||
&mut self,
|
/// to [`AnvilLevel::ignored_chunks`] or it is in view of a client.
|
||||||
chunk_x: i32,
|
///
|
||||||
chunk_z: i32,
|
/// This has no effect if a chunk at the position is already present.
|
||||||
) -> Result<Option<AnvilChunk>, ReadChunkError> {
|
pub fn force_chunk_load(&mut self, pos: ChunkPos) {
|
||||||
let region_x = chunk_x.div_euclid(32);
|
match self.pending.entry(pos) {
|
||||||
let region_z = chunk_z.div_euclid(32);
|
Entry::Occupied(oe) => {
|
||||||
|
// If the chunk is already scheduled to load but hasn't been sent to the chunk
|
||||||
let region = match self.regions.entry((region_x, region_z)) {
|
// worker yet, then give it the highest priority.
|
||||||
|
if let Some(priority) = oe.into_mut() {
|
||||||
|
*priority = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Entry::Vacant(ve) => {
|
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
|
let path = self
|
||||||
.region_root
|
.region_root
|
||||||
.join(format!("r.{region_x}.{region_z}.mca"));
|
.join(format!("r.{region_x}.{region_z}.mca"));
|
||||||
|
|
||||||
let mut file = match File::options().read(true).write(true).open(path) {
|
let mut file = match File::options().read(true).write(true).open(path) {
|
||||||
Ok(file) => file,
|
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()),
|
Err(e) => return Err(e.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,12 +167,19 @@ impl AnvilWorld {
|
||||||
|
|
||||||
file.read_exact(&mut header)?;
|
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 location_bytes = (®ion.header[chunk_idx * 4..]).read_u32::<BigEndian>()?;
|
||||||
let timestamp = (®ion.header[chunk_idx * 4 + SECTOR_SIZE..]).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_offset = (location_bytes >> 8) as u64;
|
||||||
let sector_count = (location_bytes & 0xff) as usize;
|
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
|
||||||
// If the sector offset was <2, then the chunk data would be inside the region
|
// header. That doesn't make any sense.
|
||||||
// header. That doesn't make any sense.
|
ensure!(sector_offset >= 2, "invalid chunk sector offset");
|
||||||
return Err(ReadChunkError::BadSectorOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seek to the beginning of the chunk's data.
|
// Seek to the beginning of the chunk's data.
|
||||||
region
|
region
|
||||||
|
@ -148,44 +203,267 @@ impl AnvilWorld {
|
||||||
|
|
||||||
let exact_chunk_size = region.file.read_u32::<BigEndian>()? as usize;
|
let exact_chunk_size = region.file.read_u32::<BigEndian>()? as usize;
|
||||||
|
|
||||||
if exact_chunk_size > sector_count * SECTOR_SIZE {
|
// size of this chunk in sectors must always be >= the exact size.
|
||||||
// Sector size of this chunk must always be >= the exact size.
|
ensure!(
|
||||||
return Err(ReadChunkError::BadChunkSize);
|
sector_count * SECTOR_SIZE >= exact_chunk_size,
|
||||||
}
|
"invalid chunk size"
|
||||||
|
);
|
||||||
|
|
||||||
let mut data_buf = vec![0; exact_chunk_size].into_boxed_slice();
|
let mut data_buf = vec![0; exact_chunk_size].into_boxed_slice();
|
||||||
region.file.read_exact(&mut data_buf)?;
|
region.file.read_exact(&mut data_buf)?;
|
||||||
|
|
||||||
let mut r = data_buf.as_ref();
|
let mut r = data_buf.as_ref();
|
||||||
|
|
||||||
let mut decompress_buf = vec![];
|
self.decompress_buf.clear();
|
||||||
|
|
||||||
// What compression does the chunk use?
|
// What compression does the chunk use?
|
||||||
let mut nbt_slice = match r.read_u8()? {
|
let mut nbt_slice = match r.read_u8()? {
|
||||||
// GZip
|
// GZip
|
||||||
1 => {
|
1 => {
|
||||||
let mut z = GzDecoder::new(r);
|
let mut z = GzDecoder::new(r);
|
||||||
z.read_to_end(&mut decompress_buf)?;
|
z.read_to_end(&mut self.decompress_buf)?;
|
||||||
decompress_buf.as_slice()
|
self.decompress_buf.as_slice()
|
||||||
}
|
}
|
||||||
// Zlib
|
// Zlib
|
||||||
2 => {
|
2 => {
|
||||||
let mut z = ZlibDecoder::new(r);
|
let mut z = ZlibDecoder::new(r);
|
||||||
z.read_to_end(&mut decompress_buf)?;
|
z.read_to_end(&mut self.decompress_buf)?;
|
||||||
decompress_buf.as_slice()
|
self.decompress_buf.as_slice()
|
||||||
}
|
}
|
||||||
// Uncompressed
|
// Uncompressed
|
||||||
3 => r,
|
3 => r,
|
||||||
// Unknown
|
// Unknown
|
||||||
b => return Err(ReadChunkError::UnknownCompressionScheme(b)),
|
b => bail!("unknown compression scheme number of {b}"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (data, _) = Compound::from_binary(&mut nbt_slice)?;
|
let (data, _) = Compound::from_binary(&mut nbt_slice)?;
|
||||||
|
|
||||||
if !nbt_slice.is_empty() {
|
ensure!(nbt_slice.is_empty(), "not all chunk NBT data was read");
|
||||||
return Err(ReadChunkError::IncompleteNbtRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(AnvilChunk { data, timestamp }))
|
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)]
|
#[derive(Clone, Debug, Error)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ToValenceError {
|
pub(crate) enum ParseChunkError {
|
||||||
#[error("missing chunk sections")]
|
#[error("missing chunk sections")]
|
||||||
MissingSections,
|
MissingSections,
|
||||||
#[error("missing chunk section Y")]
|
#[error("missing chunk section Y")]
|
||||||
|
@ -45,8 +45,6 @@ pub enum ToValenceError {
|
||||||
BadBiomePaletteLen,
|
BadBiomePaletteLen,
|
||||||
#[error("biome name is not a valid resource identifier")]
|
#[error("biome name is not a valid resource identifier")]
|
||||||
BadBiomeName,
|
BadBiomeName,
|
||||||
#[error("missing biome name")]
|
|
||||||
MissingBiomeName,
|
|
||||||
#[error("missing packed biome data in section")]
|
#[error("missing packed biome data in section")]
|
||||||
MissingBiomeData,
|
MissingBiomeData,
|
||||||
#[error("unexpected number of longs in biome data")]
|
#[error("unexpected number of longs in biome data")]
|
||||||
|
@ -69,27 +67,24 @@ pub enum ToValenceError {
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// - `nbt`: The Anvil chunk to read from. This is usually the value returned by
|
/// - `nbt`: The raw Anvil chunk NBT to read from.
|
||||||
/// [`AnvilWorld::read_chunk`].
|
|
||||||
/// - `chunk`: The Valence chunk to write to.
|
/// - `chunk`: The Valence chunk to write to.
|
||||||
/// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After
|
/// - `sect_offset`: A constant to add to all sector Y positions in `nbt`. After
|
||||||
/// applying the offset, only the sectors in the range
|
/// 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
|
/// - `map_biome`: A function to map biome resource identifiers in the NBT data
|
||||||
/// to Valence [`BiomeId`]s.
|
/// to Valence [`BiomeId`]s.
|
||||||
///
|
pub(crate) fn parse_chunk<F, const LOADED: bool>(
|
||||||
/// [`AnvilWorld::read_chunk`]: crate::AnvilWorld::read_chunk
|
|
||||||
pub fn to_valence<F, const LOADED: bool>(
|
|
||||||
nbt: &Compound,
|
nbt: &Compound,
|
||||||
chunk: &mut Chunk<LOADED>,
|
chunk: &mut Chunk<LOADED>,
|
||||||
sect_offset: i32,
|
sect_offset: i32,
|
||||||
mut map_biome: F,
|
mut map_biome: F,
|
||||||
) -> Result<(), ToValenceError>
|
) -> Result<(), ParseChunkError>
|
||||||
where
|
where
|
||||||
F: FnMut(Ident<&str>) -> BiomeId,
|
F: FnMut(Ident<&str>) -> BiomeId,
|
||||||
{
|
{
|
||||||
let Some(Value::List(List::Compound(sections))) = nbt.get("sections") else {
|
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![];
|
let mut converted_block_palette = vec![];
|
||||||
|
@ -97,7 +92,7 @@ where
|
||||||
|
|
||||||
for section in sections {
|
for section in sections {
|
||||||
let Some(Value::Byte(sect_y)) = section.get("Y") else {
|
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;
|
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 {
|
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 {
|
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()) {
|
if !(1..BLOCKS_PER_SECTION).contains(&palette.len()) {
|
||||||
return Err(ToValenceError::BadBlockPaletteLen);
|
return Err(ParseChunkError::BadBlockPaletteLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
converted_block_palette.clear();
|
converted_block_palette.clear();
|
||||||
|
|
||||||
for block in palette {
|
for block in palette {
|
||||||
let Some(Value::String(name)) = block.get("Name") else {
|
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 {
|
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();
|
let mut state = block_kind.to_state();
|
||||||
|
@ -135,15 +130,15 @@ where
|
||||||
if let Some(Value::Compound(properties)) = block.get("Properties") {
|
if let Some(Value::Compound(properties)) = block.get("Properties") {
|
||||||
for (key, value) in properties {
|
for (key, value) in properties {
|
||||||
let Value::String(value) = value else {
|
let Value::String(value) = value else {
|
||||||
return Err(ToValenceError::BadPropValueType)
|
return Err(ParseChunkError::BadPropValueType)
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(prop_name) = PropName::from_str(key) else {
|
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 {
|
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);
|
state = state.set(prop_name, prop_value);
|
||||||
|
@ -159,7 +154,7 @@ where
|
||||||
debug_assert!(converted_block_palette.len() > 1);
|
debug_assert!(converted_block_palette.len() > 1);
|
||||||
|
|
||||||
let Some(Value::LongArray(data)) = block_states.get("data") else {
|
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);
|
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;
|
let mask = 2_u64.pow(bits_per_idx as u32) - 1;
|
||||||
|
|
||||||
if long_count != data.len() {
|
if long_count != data.len() {
|
||||||
return Err(ToValenceError::BadBlockLongCount);
|
return Err(ParseChunkError::BadBlockLongCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
@ -183,7 +178,7 @@ where
|
||||||
let idx = (u64 >> (bits_per_idx * j)) & mask;
|
let idx = (u64 >> (bits_per_idx * j)) & mask;
|
||||||
|
|
||||||
let Some(block) = converted_block_palette.get(idx as usize).cloned() else {
|
let Some(block) = converted_block_palette.get(idx as usize).cloned() else {
|
||||||
return Err(ToValenceError::BadBlockPaletteIndex)
|
return Err(ParseChunkError::BadBlockPaletteIndex)
|
||||||
};
|
};
|
||||||
|
|
||||||
let x = i % 16;
|
let x = i % 16;
|
||||||
|
@ -198,22 +193,22 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(Value::Compound(biomes)) = section.get("biomes") else {
|
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 {
|
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()) {
|
if !(1..BIOMES_PER_SECTION).contains(&palette.len()) {
|
||||||
return Err(ToValenceError::BadBiomePaletteLen);
|
return Err(ParseChunkError::BadBiomePaletteLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
converted_biome_palette.clear();
|
converted_biome_palette.clear();
|
||||||
|
|
||||||
for biome_name in palette {
|
for biome_name in palette {
|
||||||
let Ok(ident) = Ident::<Cow<str>>::new(biome_name) else {
|
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()));
|
converted_biome_palette.push(map_biome(ident.as_str_ident()));
|
||||||
|
@ -225,7 +220,7 @@ where
|
||||||
debug_assert!(converted_biome_palette.len() > 1);
|
debug_assert!(converted_biome_palette.len() > 1);
|
||||||
|
|
||||||
let Some(Value::LongArray(data)) = biomes.get("data") else {
|
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);
|
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;
|
let mask = 2_u64.pow(bits_per_idx as u32) - 1;
|
||||||
|
|
||||||
if long_count != data.len() {
|
if long_count != data.len() {
|
||||||
return Err(ToValenceError::BadBiomeLongCount);
|
return Err(ParseChunkError::BadBiomeLongCount);
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
@ -249,7 +244,7 @@ where
|
||||||
let idx = (u64 >> (bits_per_idx * j)) & mask;
|
let idx = (u64 >> (bits_per_idx * j)) & mask;
|
||||||
|
|
||||||
let Some(biome) = converted_biome_palette.get(idx as usize).cloned() else {
|
let Some(biome) = converted_biome_palette.get(idx as usize).cloned() else {
|
||||||
return Err(ToValenceError::BadBiomePaletteIndex)
|
return Err(ParseChunkError::BadBiomePaletteIndex)
|
||||||
};
|
};
|
||||||
|
|
||||||
let x = i % 4;
|
let x = i % 4;
|
||||||
|
@ -265,41 +260,41 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(Value::List(block_entities)) = nbt.get("block_entities") else {
|
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 {
|
if let List::Compound(block_entities) = block_entities {
|
||||||
for comp in block_entities {
|
for comp in block_entities {
|
||||||
let Some(Value::String(ident)) = comp.get("id") else {
|
let Some(Value::String(ident)) = comp.get("id") else {
|
||||||
return Err(ToValenceError::MissingBlockEntityIdent);
|
return Err(ParseChunkError::MissingBlockEntityIdent);
|
||||||
};
|
};
|
||||||
let Ok(ident) = Ident::new(&ident[..]) else {
|
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 {
|
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 {
|
let block_entity = BlockEntity {
|
||||||
kind,
|
kind,
|
||||||
nbt: comp.clone(),
|
nbt: comp.clone(),
|
||||||
};
|
};
|
||||||
let Some(Value::Int(x)) = comp.get("x") else {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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);
|
chunk.set_block_entity(x, y, z, block_entity);
|
|
@ -72,7 +72,6 @@ use valence_registry::tags::TagsRegistry;
|
||||||
use valence_registry::RegistrySet;
|
use valence_registry::RegistrySet;
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
pub mod chat;
|
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod custom_payload;
|
pub mod custom_payload;
|
||||||
pub mod event_loop;
|
pub mod event_loop;
|
||||||
|
@ -81,6 +80,7 @@ pub mod interact_block;
|
||||||
pub mod interact_entity;
|
pub mod interact_entity;
|
||||||
pub mod interact_item;
|
pub mod interact_item;
|
||||||
pub mod keepalive;
|
pub mod keepalive;
|
||||||
|
pub mod message;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod op_level;
|
pub mod op_level;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
|
@ -107,8 +107,10 @@ pub struct FlushPacketsSet;
|
||||||
|
|
||||||
pub struct SpawnClientsSet;
|
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)]
|
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
struct UpdateClientsSet;
|
pub struct UpdateClientsSet;
|
||||||
|
|
||||||
impl Plugin for ClientPlugin {
|
impl Plugin for ClientPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
|
@ -150,7 +152,7 @@ impl Plugin for ClientPlugin {
|
||||||
action::build(app);
|
action::build(app);
|
||||||
teleport::build(app);
|
teleport::build(app);
|
||||||
weather::build(app);
|
weather::build(app);
|
||||||
chat::build(app);
|
message::build(app);
|
||||||
custom_payload::build(app);
|
custom_payload::build(app);
|
||||||
hand_swing::build(app);
|
hand_swing::build(app);
|
||||||
interact_block::build(app);
|
interact_block::build(app);
|
||||||
|
|
|
@ -7,7 +7,6 @@ use valence_core::protocol::packet::chat::{ChatMessageC2s, GameMessageS2c};
|
||||||
use valence_core::text::Text;
|
use valence_core::text::Text;
|
||||||
|
|
||||||
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
use crate::event_loop::{EventLoopSchedule, EventLoopSet, PacketEvent};
|
||||||
use crate::Client;
|
|
||||||
|
|
||||||
pub(super) fn build(app: &mut App) {
|
pub(super) fn build(app: &mut App) {
|
||||||
app.add_event::<ChatMessageEvent>().add_system(
|
app.add_event::<ChatMessageEvent>().add_system(
|
||||||
|
@ -17,22 +16,34 @@ pub(super) fn build(app: &mut App) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
pub trait SendMessage {
|
||||||
pub struct ChatMessageEvent {
|
/// Sends a system message visible in the chat.
|
||||||
pub client: Entity,
|
fn send_chat_message(&mut self, msg: impl Into<Text>);
|
||||||
pub message: Box<str>,
|
/// Displays a message in the player's action bar (text above the hotbar).
|
||||||
pub timestamp: u64,
|
fn send_action_bar_message(&mut self, msg: impl Into<Text>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl<T: WritePacket> SendMessage for T {
|
||||||
/// Sends a system message to the player which is visible in the chat. The
|
fn send_chat_message(&mut self, msg: impl Into<Text>) {
|
||||||
/// message is only visible to this client.
|
|
||||||
pub fn send_message(&mut self, msg: impl Into<Text>) {
|
|
||||||
self.write_packet(&GameMessageS2c {
|
self.write_packet(&GameMessageS2c {
|
||||||
chat: msg.into().into(),
|
chat: msg.into().into(),
|
||||||
overlay: false,
|
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(
|
pub fn handle_chat_message(
|
|
@ -64,7 +64,8 @@ mod paletted_container;
|
||||||
pub struct InstancePlugin;
|
pub struct InstancePlugin;
|
||||||
|
|
||||||
/// When Minecraft entity changes are written to the packet buffers of chunks.
|
/// 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)]
|
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct WriteUpdatePacketsToInstancesSet;
|
pub struct WriteUpdatePacketsToInstancesSet;
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,8 @@
|
||||||
//! ## Access other world border properties.
|
//! ## Access other world border properties.
|
||||||
//! Access to the rest of the world border properties is fairly straightforward
|
//! Access to the rest of the world border properties is fairly straightforward
|
||||||
//! by querying their respective component. [`WorldBorderBundle`] contains
|
//! 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)]
|
#![allow(clippy::type_complexity)]
|
||||||
#![deny(
|
#![deny(
|
||||||
rustdoc::broken_intra_doc_links,
|
rustdoc::broken_intra_doc_links,
|
||||||
|
|
Loading…
Reference in a new issue