Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anvil Rework #367

Merged
merged 7 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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