Skip to content

Commit

Permalink
Anvil Rework (#367)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
rj00a authored Jun 15, 2023
1 parent 61f2279 commit 2ed5a88
Show file tree
Hide file tree
Showing 23 changed files with 566 additions and 335 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +19,7 @@ graph TD
instance --> entity
player_list --> client
inventory --> client
anvil --> instance
anvil --> client
entity --> block
advancement --> client
world_border --> client
Expand Down
2 changes: 2 additions & 0 deletions crates/valence/benches/anvil.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/*
use std::fs::create_dir_all;
use std::hint::black_box;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -133,3 +134,4 @@ fn get_world_asset(
Ok(final_path)
}
*/
2 changes: 1 addition & 1 deletion crates/valence/benches/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod var_long;

criterion_group! {
benches,
anvil::load,
// anvil::load,
block::block,
decode_array::decode_array,
idle::idle_update,
Expand Down
202 changes: 61 additions & 141 deletions crates/valence/examples/anvil_loading.rs
Original file line number Diff line number Diff line change
@@ -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();
}

Expand All @@ -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(
Expand All @@ -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();

for (client, view, old_view) in &mut clients {
let view = view.get();
let queue_pos = |pos| {
if instance.chunk(pos).is_none() {
match state.pending.entry(pos) {
Entry::Occupied(mut oe) => {
if let Some(priority) = oe.get_mut() {
let dist = view.pos.distance_squared(pos);
*priority = (*priority).min(dist);
}
}
Entry::Vacant(ve) => {
let dist = view.pos.distance_squared(pos);
ve.insert(Some(dist));
}
}
let mut inst = instances.single_mut();

for event in events.iter() {
match &event.status {
ChunkLoadStatus::Success { .. } => {
// The chunk was inserted into the world. Nothing for us to do.
}
};

// Queue all the new chunks in the view to be sent to the anvil worker.
if client.is_added() {
view.iter().for_each(queue_pos);
} else {
let old_view = old_view.get();
if old_view != view {
view.diff(old_view).for_each(queue_pos);
ChunkLoadStatus::Empty => {
// There's no chunk here so let's insert an empty chunk. If we were doing
// terrain generation we would prepare that here.
inst.insert_chunk(event.pos, Chunk::default());
}
}
}
}

fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut<GameState>) {
let mut instance = instances.single_mut();
let state = state.into_inner();

// Insert the chunks that are finished loading into the instance.
for (pos, chunk) in state.receiver.drain() {
instance.insert_chunk(pos, chunk);
assert!(state.pending.remove(&pos).is_some());
}

// Collect all the new chunks that need to be loaded this tick.
let mut to_send = vec![];

for (pos, priority) in &mut state.pending {
if let Some(pri) = priority.take() {
to_send.push((pri, pos));
}
}

// Sort chunks by ascending priority.
to_send.sort_unstable_by_key(|(pri, _)| *pri);

// Send the sorted chunks to be loaded.
for (_, pos) in to_send {
let _ = state.sender.try_send(*pos);
}
}

fn anvil_worker(
receiver: Receiver<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));
}
ChunkLoadStatus::Failed(e) => {
// Something went wrong.
eprintln!(
"failed to load chunk at ({}, {}): {e:#}",
event.pos.x, event.pos.z
);
inst.insert_chunk(event.pos, Chunk::default());
}
Err(e) => warn!("Failed to get chunk at ({}, {}): {e:#}.", pos.x, pos.z),
}
}
}

fn get_chunk(pos: ChunkPos, world: &mut AnvilWorld) -> anyhow::Result<Option<Chunk>> {
let Some(AnvilChunk { data, .. }) = world.read_chunk(pos.x, pos.z)? else {
return Ok(None)
};
// Display the number of loaded chunks in the action bar of all clients.
fn display_loaded_chunk_count(mut instances: Query<&mut Instance>, mut last_count: Local<usize>) {
let mut inst = instances.single_mut();

let mut chunk = Chunk::new(SECTION_COUNT);
let cnt = inst.chunks().count();

valence_anvil::to_valence(&data, &mut chunk, 4, |_| BiomeId::default())?;

Ok(Some(chunk))
if *last_count != cnt {
*last_count = cnt;
inst.send_action_bar_message(
"Chunk Count: ".into_text() + (cnt as i32).color(Color::LIGHT_PURPLE),
);
}
}
2 changes: 1 addition & 1 deletion crates/valence/examples/block_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
3 changes: 2 additions & 1 deletion crates/valence/examples/building.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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());
}
}

Expand Down
5 changes: 3 additions & 2 deletions crates/valence/examples/conway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
3 changes: 2 additions & 1 deletion crates/valence/examples/death.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(),
);
}
Expand Down
3 changes: 2 additions & 1 deletion crates/valence/examples/entity_hitbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}

Expand Down
Loading

0 comments on commit 2ed5a88

Please sign in to comment.