mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-10 14:51:30 +11:00
9c9f672a22
## Description Closes #291 - Update extractors to support Minecraft 1.19.4 - Update code generators. - Changed generated entity component names to avoid name collisions. - Update `glam` version. - Added `Encode` and `Decode` for `glam` types in `valence_protocol`. - Fixed inconsistent packet names and assign packet IDs automatically. - Remove `ident` and rename `ident_str` to `ident`. - Rework registry codec configuration. Biomes and dimensions exist as entities.`BiomeRegistry` and `DimensionTypeRegistry` resources have been added. The vanilla registry codec is loaded at startup. ### Issues - Creating new instances has become more tedious than it should be. This will be addressed later. ## Test Plan Steps: 1. Boot up a vanilla server with online mode disabled. 2. Run the `packet_inspector`. 3. Connect to the vanilla server through the packet inspector to ensure all packets are updated correctly. 4. Close the vanilla server and try some valence examples.
212 lines
6.2 KiB
Rust
212 lines
6.2 KiB
Rust
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::client::{default_event_handler, despawn_disconnected_clients};
|
|
use valence::entity::player::PlayerEntityBundle;
|
|
use valence::prelude::*;
|
|
use valence_anvil::{AnvilChunk, AnvilWorld};
|
|
|
|
const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0);
|
|
const SECTION_COUNT: usize = 24;
|
|
|
|
#[derive(Parser)]
|
|
#[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());
|
|
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,
|
|
};
|
|
|
|
App::new()
|
|
.add_plugin(ServerPlugin::new(()))
|
|
.insert_resource(game_state)
|
|
.add_startup_system(setup)
|
|
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
|
|
.add_systems(
|
|
(
|
|
init_clients,
|
|
remove_unviewed_chunks,
|
|
update_client_views,
|
|
send_recv_chunks,
|
|
)
|
|
.chain(),
|
|
)
|
|
.add_system(despawn_disconnected_clients)
|
|
.run();
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
dimensions: Query<&DimensionType>,
|
|
biomes: Query<&Biome>,
|
|
server: Res<Server>,
|
|
) {
|
|
let instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server);
|
|
commands.spawn(instance);
|
|
}
|
|
|
|
fn init_clients(
|
|
mut clients: Query<(Entity, &mut GameMode, &mut IsFlat, &UniqueId), Added<Client>>,
|
|
instances: Query<Entity, With<Instance>>,
|
|
mut commands: Commands,
|
|
) {
|
|
for (entity, mut game_mode, mut is_flat, uuid) in &mut clients {
|
|
*game_mode = GameMode::Creative;
|
|
is_flat.0 = true;
|
|
|
|
commands.entity(entity).insert(PlayerEntityBundle {
|
|
location: Location(instances.single()),
|
|
position: Position(SPAWN_POS),
|
|
uuid: *uuid,
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
|
|
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>,
|
|
) {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
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))
|
|
}
|