From afc4cb0cd321db69a67694147e6276c5eac155d4 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 20 Jan 2026 18:59:05 +0100 Subject: [PATCH] chore: this should speed things up --- pumpkin-config/src/world.rs | 2 +- pumpkin-data/build/block.rs | 18 ++ pumpkin-data/build/tag.rs | 102 ++++------- pumpkin-data/src/blocks.rs | 12 ++ pumpkin-world/src/block/state.rs | 5 + pumpkin-world/src/chunk/io/file_manager.rs | 60 +++---- pumpkin-world/src/chunk/mod.rs | 18 +- pumpkin-world/src/chunk/palette.rs | 14 +- pumpkin-world/src/chunk_system.rs | 6 +- .../src/generation/feature/placed_features.rs | 5 +- pumpkin-world/src/generation/proto_chunk.rs | 10 +- .../src/generation/surface/terrain.rs | 6 +- pumpkin-world/src/level.rs | 164 +++++++---------- pumpkin/src/block/blocks/carpet.rs | 6 +- pumpkin/src/entity/player.rs | 32 +--- pumpkin/src/lib.rs | 34 +++- pumpkin/src/net/java/login.rs | 11 +- pumpkin/src/server/key_store.rs | 5 + pumpkin/src/server/mod.rs | 167 ++++++++++++------ pumpkin/src/world/chunker.rs | 129 +++++++------- pumpkin/src/world/mod.rs | 7 +- 21 files changed, 430 insertions(+), 383 deletions(-) diff --git a/pumpkin-config/src/world.rs b/pumpkin-config/src/world.rs index be38d9a04..33c01145e 100644 --- a/pumpkin-config/src/world.rs +++ b/pumpkin-config/src/world.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::chunk::ChunkConfig; -#[derive(Deserialize, Serialize, Default)] +#[derive(Deserialize, Serialize, Default, Clone)] pub struct LevelConfig { pub chunk: ChunkConfig, // TODO: More options diff --git a/pumpkin-data/build/block.rs b/pumpkin-data/build/block.rs index 190d90b4c..b095463c6 100644 --- a/pumpkin-data/build/block.rs +++ b/pumpkin-data/build/block.rs @@ -428,12 +428,17 @@ impl PistonBehavior { } impl BlockState { + const IS_AIR: u16 = 1 << 0; const HAS_RANDOM_TICKS: u16 = 1 << 9; fn has_random_ticks(&self) -> bool { self.state_flags & Self::HAS_RANDOM_TICKS != 0 } + pub const fn is_air(&self) -> bool { + self.state_flags & Self::IS_AIR != 0 + } + fn to_tokens(&self) -> TokenStream { let mut tokens = TokenStream::new(); let id = LitInt::new(&self.id.to_string(), Span::call_site()); @@ -659,6 +664,8 @@ pub(crate) fn build() -> TokenStream { .collect(); let mut random_tick_states = Vec::new(); + let mut air_states = Vec::new(); + let mut constants_list = Vec::new(); let mut block_from_name_entries = Vec::new(); let mut block_from_item_id_arms = Vec::new(); @@ -679,6 +686,10 @@ pub(crate) fn build() -> TokenStream { let state_id = LitInt::new(&state.id.to_string(), Span::call_site()); random_tick_states.push(state_id); } + if state.is_air() { + let state_id = LitInt::new(&state.id.to_string(), Span::call_site()); + air_states.push(state_id); + } } let mut property_collection = HashSet::new(); @@ -817,6 +828,8 @@ pub(crate) fn build() -> TokenStream { .iter() .map(|shape| shape.to_token_stream()); + let air_state_ids = quote! { #(#air_states)|* }; + let block_props = block_properties.iter().map(|prop| prop.to_token_stream()); let properties = property_enums.values().map(|prop| prop.to_token_stream()); @@ -908,6 +921,11 @@ pub(crate) fn build() -> TokenStream { #(#block_entity_types),* ]; + #[inline(always)] + pub fn is_air(state_id: u16) -> bool { + matches!(state_id, #air_state_ids) + } + #[inline(always)] pub fn has_random_ticks(state_id: u16) -> bool { #mod_ident::#contains_ident(state_id) diff --git a/pumpkin-data/build/tag.rs b/pumpkin-data/build/tag.rs index 8927579fa..7d61c2cf5 100644 --- a/pumpkin-data/build/tag.rs +++ b/pumpkin-data/build/tag.rs @@ -77,6 +77,14 @@ pub(crate) fn build() -> TokenStream { } .to_token_stream(); + let block_id_map: BTreeMap = blocks_assets + .blocks + .iter() + .map(|b| (b.name.clone(), b.id)) + .collect(); + let fluid_id_map: BTreeMap = + fluids.iter().map(|f| (f.name.clone(), f.id)).collect(); + // Generate tag arrays for each registry key let mut tag_dicts = Vec::new(); let mut match_arms_value = Vec::new(); @@ -84,80 +92,48 @@ pub(crate) fn build() -> TokenStream { let mut match_arms_tags_all = Vec::new(); let mut tag_identifiers = Vec::new(); - for (key, tag_map) in tags.into_iter() { + for (key, tag_map) in tags { let key_pascal = format_ident!("{}", key.to_pascal_case()); let dict_name = format_ident!("{}_TAGS", key.to_pascal_case().to_uppercase()); - // Create a BTreeMap to store tag name -> index mapping - let mut tag_values = Vec::new(); + let mut tag_entries = Vec::new(); + let mut tag_map_entries = Vec::new(); - // Collect all unique tags for (tag_name, values) in tag_map { - tag_values.push((tag_name, values)); + let ids: Vec = values + .iter() + .filter_map(|v| match key.as_str() { + "worldgen/biome" => biomes.get(v).map(|b| b.id as u16), + "fluid" => fluid_id_map.get(v).copied(), + "item" => items.get(v).map(|i| i.id), + "block" => block_id_map.get(v).copied(), + "enchantment" => enchantments + .get(&format!("minecraft:{}", v)) + .map(|e| e.id as u16), + "entity_type" => entities.get(v).map(|e| e.id), + _ => None, + }) + .collect(); + let tag_const_name = + format_ident!("{}", tag_name.replace([':', '/'], "_").to_uppercase()); + + tag_entries.push(quote! { + pub const #tag_const_name: Tag = (&[#(#values),*], &[#(#ids),*]); + }); + + tag_map_entries.push(quote! { + #tag_name => &#key_pascal::#tag_const_name + }); } - tag_values.sort(); - - // Generate the static array of tag values - let tag_array_entries = tag_values - .iter() - .map(|(tag_name, values)| { - let tag_values_array = values.iter().map(|v| quote! { #v }).collect::>(); - let tag_id_array = match &key { - t if t == "worldgen/biome" => values.iter().map(|v| { - let id = biomes.get(v).unwrap().id as u16; - quote! { #id } - }).collect::>(), - t if t == "fluid" => values.iter().map(|v| { - let id = fluids.iter().find(|i| { &i.name == v }).unwrap().id; - quote! { #id } - }).collect::>(), - t if t == "item" => values.iter().map(|v| { - let id = items.get(v).unwrap().id; - quote! { #id } - }).collect::>(), - t if t == "block" => values.iter().map(|v| { - let id = blocks_assets.blocks.iter().find(|i| { &i.name == v }).unwrap().id; - quote! { #id } - }).collect::>(), - t if t == "enchantment" => values.iter().map(|v| { - let id = enchantments.get(&("minecraft:".to_string() + v)).unwrap().id as u16; - quote! { #id } - }).collect::>(), - t if t == "entity_type" => values.iter().map(|v| { - let id = entities.get(v).unwrap().id; - quote! { #id } - }).collect::>(), - _ => Vec::new(), - }; - let mapped_name = format_ident!("{}", tag_name.replace(":", "_").replace("/", "_").to_uppercase()); - quote! { - pub const #mapped_name: Tag = (&[#(#tag_values_array),*], &[#(#tag_id_array),*]); - } - }) - .collect::>(); - let tag_array_entries_map = tag_values - .iter() - .map(|(tag_name, _values)| { - let mapped_name = format_ident!( - "{}", - tag_name.replace(":", "_").replace("/", "_").to_uppercase() - ); - quote! { - #tag_name => &#key_pascal::#mapped_name - } - }) - .collect::>(); - // Add the static array declaration tag_dicts.push(quote! { #[allow(non_snake_case)] pub mod #key_pascal { - use crate::tag::Tag; - - #(#tag_array_entries)* + use super::Tag; + #(#tag_entries)* } - static #dict_name: phf::Map<&str, &'static Tag> = phf::phf_map! { - #(#tag_array_entries_map),* + static #dict_name: phf::Map<&'static str, &'static Tag> = phf::phf_map! { + #(#tag_map_entries),* }; }); diff --git a/pumpkin-data/src/blocks.rs b/pumpkin-data/src/blocks.rs index 410b5e158..b7c45e525 100644 --- a/pumpkin-data/src/blocks.rs +++ b/pumpkin-data/src/blocks.rs @@ -27,6 +27,18 @@ pub struct Block { pub experience: Option, } +impl PartialEq for Block { + fn eq(&self, other: &u16) -> bool { + self.id == *other + } +} + +impl PartialEq for u16 { + fn eq(&self, other: &Block) -> bool { + *self == other.id + } +} + impl PartialEq for Block { fn eq(&self, other: &Self) -> bool { self.id == other.id diff --git a/pumpkin-world/src/block/state.rs b/pumpkin-world/src/block/state.rs index 7aca87e7b..823d145dc 100644 --- a/pumpkin-world/src/block/state.rs +++ b/pumpkin-world/src/block/state.rs @@ -18,4 +18,9 @@ impl RawBlockState { pub fn to_block(&self) -> &'static Block { Block::from_state_id(self.0) } + + #[inline] + pub fn to_block_id(&self) -> u16 { + Block::get_raw_id_from_state_id(self.0) + } } diff --git a/pumpkin-world/src/chunk/io/file_manager.rs b/pumpkin-world/src/chunk/io/file_manager.rs index 9ac7a8fb7..9b4f0f9fb 100644 --- a/pumpkin-world/src/chunk/io/file_manager.rs +++ b/pumpkin-world/src/chunk/io/file_manager.rs @@ -1,7 +1,6 @@ use std::{ collections::BTreeMap, io::ErrorKind, - ops::{AddAssign, SubAssign}, path::{Path, PathBuf}, sync::Arc, }; @@ -162,62 +161,60 @@ where { type Data = Arc>; - // Changed: Return BoxFuture<()> fn watch_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks: &'a [Vector2], ) -> BoxFuture<'a, ()> { + let paths: Vec<_> = chunks + .iter() + .map(|chunk| P::file_path(folder, &S::get_chunk_key(chunk))) + .collect(); + Box::pin(async move { - // It is intentional that regions are watched multiple times (once per chunk) let mut watchers = self.watchers.write().await; - for chunk in chunks { - let key = S::get_chunk_key(chunk); - let map_key = P::file_path(folder, &key); - match watchers.entry(map_key) { - std::collections::btree_map::Entry::Vacant(vacant) => { - let _ = vacant.insert(1); - } - std::collections::btree_map::Entry::Occupied(mut occupied) => { - occupied.get_mut().add_assign(1); - } - } + for path in paths { + watchers + .entry(path) + .and_modify(|count| *count += 1) + .or_insert(1); } }) } - // Changed: Return BoxFuture<()> fn unwatch_chunks<'a>( &'a self, folder: &'a LevelFolder, chunks: &'a [Vector2], ) -> BoxFuture<'a, ()> { + let paths: Vec<_> = chunks + .iter() + .map(|chunk| P::file_path(folder, &S::get_chunk_key(chunk))) + .collect(); Box::pin(async move { let mut watchers = self.watchers.write().await; - for chunk in chunks { - let key = S::get_chunk_key(chunk); - let map_key = P::file_path(folder, &key); - match watchers.entry(map_key) { - std::collections::btree_map::Entry::Vacant(_vacant) => {} - std::collections::btree_map::Entry::Occupied(mut occupied) => { - occupied.get_mut().sub_assign(1); - if occupied.get().is_zero() { - occupied.remove_entry(); - } + + for path in paths { + if let std::collections::btree_map::Entry::Occupied(mut occupied) = + watchers.entry(path) + { + let count = occupied.get_mut(); + *count = count.saturating_sub(1); + + if *count == 0 { + occupied.remove(); } } } }) } - // Changed: Return BoxFuture<()> fn clear_watched_chunks(&self) -> BoxFuture<'_, ()> { Box::pin(async move { self.watchers.write().await.clear(); }) } - // Changed: Return BoxFuture<()> fn fetch_chunks<'a>( &'a self, folder: &'a LevelFolder, @@ -284,7 +281,6 @@ where }) } - // Changed: Return BoxFuture> fn save_chunks<'a>( &'a self, folder: &'a LevelFolder, @@ -374,8 +370,6 @@ where // Decrement strong count drop(chunk_serializer); - // If there are still no watchers, drop from the locks - let mut locks = self.file_locks.write().await; if self .watchers @@ -384,6 +378,8 @@ where .get(&path) .is_none_or(|count| count.is_zero()) { + let mut locks = self.file_locks.write().await; + let can_remove = if let Some(loader) = locks.get(&path) { loader.can_remove().await } else { @@ -410,7 +406,6 @@ where }) } - // Changed: Return BoxFuture<()> fn clean_up_log(&self) -> BoxFuture<'_, ()> { Box::pin(async move { let locks = self.file_locks.read().await; @@ -418,11 +413,10 @@ where }) } - // Changed: Return BoxFuture<()> fn block_and_await_ongoing_tasks(&self) -> BoxFuture<'_, ()> { Box::pin(async move { //we need to block any other operation - let serializer_cache = self.file_locks.write().await; + let serializer_cache = self.file_locks.read().await; // Acquire a write lock on all entries to verify they are complete let tasks = serializer_cache diff --git a/pumpkin-world/src/chunk/mod.rs b/pumpkin-world/src/chunk/mod.rs index 7794df66a..ed1aea53a 100644 --- a/pumpkin-world/src/chunk/mod.rs +++ b/pumpkin-world/src/chunk/mod.rs @@ -3,7 +3,7 @@ use crate::block::entities::BlockEntity; use crate::chunk::format::LightContainer; use crate::tick::scheduler::ChunkTickScheduler; use palette::{BiomePalette, BlockPalette}; -use pumpkin_data::block_properties::blocks_movement; +use pumpkin_data::block_properties::{blocks_movement, is_air}; use pumpkin_data::chunk::ChunkStatus; use pumpkin_data::fluid::Fluid; use pumpkin_data::tag::Block::MINECRAFT_LEAVES; @@ -438,7 +438,7 @@ impl ChunkSections { let mut y = first_y; while y >= self.min_y { if let Some(block_state_id) = self.get_block_absolute_y(relative_x, y, relative_z) - && !BlockState::from_id(block_state_id).is_air() + && !is_air(block_state_id) { return Some(y); } @@ -568,12 +568,14 @@ impl ChunkData { } pub fn get_highest_non_empty_subchunk(&self) -> usize { - for (i, sub_chunk) in self.section.sections.iter().enumerate().rev() { - if sub_chunk.block_states.non_air_block_count() != 0 { - return i; - } - } - 0 + self.section + .sections + .iter() + .enumerate() + .rev() + .position(|(_, sub)| !sub.block_states.has_only_air()) + .map(|p| self.section.sections.len() - 1 - p) + .unwrap_or(0) } } diff --git a/pumpkin-world/src/chunk/palette.rs b/pumpkin-world/src/chunk/palette.rs index b8c10296d..8524b3e34 100644 --- a/pumpkin-world/src/chunk/palette.rs +++ b/pumpkin-world/src/chunk/palette.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, hash::Hash}; -use pumpkin_data::{Block, BlockState, chunk::Biome}; +use pumpkin_data::{Block, BlockState, block_properties::is_air, chunk::Biome}; use pumpkin_util::encompassing_bits; use crate::block::BlockStateCodec; @@ -475,10 +475,18 @@ impl BlockPalette { } } + /// Check if the entire chunk is filled with only air + pub fn has_only_air(&self) -> bool { + match self { + Self::Homogeneous(id) => is_air(*id), + Self::Heterogeneous(data) => data.palette.iter().all(|&id| is_air(id)), + } + } + pub fn non_air_block_count(&self) -> u16 { match self { Self::Homogeneous(registry_id) => { - if !BlockState::from_id(*registry_id).is_air() { + if !is_air(*registry_id) { Self::VOLUME as u16 } else { 0 @@ -489,7 +497,7 @@ impl BlockPalette { .iter() .zip(data.counts.iter()) .filter_map(|(registry_id, count)| { - if !BlockState::from_id(*registry_id).is_air() { + if !is_air(*registry_id) { Some(*count) } else { None diff --git a/pumpkin-world/src/chunk_system.rs b/pumpkin-world/src/chunk_system.rs index 75da0cb2a..8da3ad8ca 100644 --- a/pumpkin-world/src/chunk_system.rs +++ b/pumpkin-world/src/chunk_system.rs @@ -9,6 +9,7 @@ TODO use crate::block::RawBlockState; use crate::chunk::io::LoadedData::Loaded; use crate::chunk::{ChunkData, ChunkHeightmapType, ChunkLight, ChunkSections, SubChunk}; +use pumpkin_data::block_properties::is_air; use pumpkin_data::dimension::Dimension; use std::default::Default; use std::pin::Pin; @@ -825,6 +826,7 @@ impl Chunk { } } } + let mut chunk = ChunkData { light_engine: ChunkLight { sky_light: (0..sections.sections.len()) @@ -1140,9 +1142,7 @@ impl GenerationCache for Cache { } fn is_air(&self, local_pos: &Vector3) -> bool { - GenerationCache::get_block_state(self, local_pos) - .to_state() - .is_air() + is_air(GenerationCache::get_block_state(self, local_pos).0) } } diff --git a/pumpkin-world/src/generation/feature/placed_features.rs b/pumpkin-world/src/generation/feature/placed_features.rs index a1a6c9c62..705e83f9b 100644 --- a/pumpkin-world/src/generation/feature/placed_features.rs +++ b/pumpkin-world/src/generation/feature/placed_features.rs @@ -1,3 +1,4 @@ +use pumpkin_data::block_properties::is_air; use pumpkin_data::{Block, BlockDirection}; use pumpkin_util::{HeightMap, include_json_static}; use serde::Deserialize; @@ -366,8 +367,8 @@ impl CountOnEveryLayerPlacementModifier { } fn blocks_spawn(state: &RawBlockState) -> bool { - let block = state.to_block(); - state.to_state().is_air() || block == &Block::WATER || block == &Block::LAVA + let block = state.to_block_id(); + is_air(state.0) || block == Block::WATER || block == Block::LAVA } } diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 742eaaecc..8472b8859 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::pin::Pin; +use pumpkin_data::block_properties::is_air; use pumpkin_data::dimension::Dimension; use pumpkin_data::fluid::{Fluid, FluidState}; use pumpkin_data::tag; @@ -430,8 +431,7 @@ impl ProtoChunk { #[inline] pub fn is_air(&self, local_pos: &Vector3) -> bool { - let state = self.get_block_state(local_pos).to_state(); - state.is_air() + is_air(self.get_block_state(local_pos).0) } #[inline] @@ -815,12 +815,10 @@ impl ProtoChunk { let state = self .get_block_state(&Vector3::new(local_x, search_y, local_z)) - .to_block(); + .to_block_id(); // TODO: Is there a better way to check that its not a fluid? - if !(state != &AIR_BLOCK - && state != &WATER_BLOCK - && state != &LAVA_BLOCK) + if !(state != AIR_BLOCK && state != WATER_BLOCK && state != LAVA_BLOCK) { min = search_y + 1; break; diff --git a/pumpkin-world/src/generation/surface/terrain.rs b/pumpkin-world/src/generation/surface/terrain.rs index bf6bd5d5e..339075351 100644 --- a/pumpkin-world/src/generation/surface/terrain.rs +++ b/pumpkin-world/src/generation/surface/terrain.rs @@ -152,12 +152,12 @@ impl SurfaceTerrainBuilder { if surface_y <= elevation_y { for y in (chunk.bottom_y() as i32..=elevation_y).rev() { let pos = Vector3::new(global_x, y, global_z); - let block_state = chunk.get_block_state(&pos).to_block(); - if block_state == Block::from_state_id(chunk.default_block.id) { + let block_state = chunk.get_block_state(&pos).to_block_id(); + if block_state == Block::get_raw_id_from_state_id(chunk.default_block.id) { break; } - if block_state == &WATER_BLOCK { + if block_state == WATER_BLOCK { return; } } diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index f306f272f..60370944d 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -13,7 +13,7 @@ use crate::{ world::BlockRegistryExt, }; use crossbeam::channel::Sender; -use dashmap::{DashMap, Entry}; +use dashmap::DashMap; use log::trace; use num_traits::Zero; use pumpkin_config::{chunk::ChunkConfig, world::LevelConfig}; @@ -35,7 +35,6 @@ use std::{ thread, }; // use tokio::runtime::Handle; -use tokio::time::Instant; use tokio::{ select, sync::{ @@ -134,45 +133,40 @@ impl Level { seed: i64, dimension: Dimension, ) -> Arc { - // If we are using an already existing world we want to read the seed from the level.dat, If not we want to check if there is a seed in the config, if not lets create a random one let region_folder = root_folder.join("region"); - if !region_folder.exists() { - std::fs::create_dir_all(®ion_folder).expect("Failed to create Region folder"); - } let entities_folder = root_folder.join("entities"); - if !entities_folder.exists() { - std::fs::create_dir_all(®ion_folder).expect("Failed to create Entities folder"); - } + + std::fs::create_dir_all(®ion_folder).expect("Failed to create Region folder"); + std::fs::create_dir_all(&entities_folder).expect("Failed to create Entities folder"); + let level_folder = LevelFolder { root_folder, region_folder, entities_folder, }; - // TODO: Load info correctly based on world format type let seed = Seed(seed as u64); let world_gen = get_world_gen(seed, dimension).into(); let chunk_saver: Arc> = match &level_config.chunk { - ChunkConfig::Linear(chunk_config) => Arc::new( - ChunkFileManager::>::new(chunk_config.clone()), + ChunkConfig::Linear(config) => Arc::new( + ChunkFileManager::>::new(config.clone()), + ), + ChunkConfig::Anvil(config) => Arc::new( + ChunkFileManager::>::new(config.clone()), ), - ChunkConfig::Anvil(chunk_config) => Arc::new(ChunkFileManager::< - AnvilChunkFile, - >::new(chunk_config.clone())), }; let entity_saver: Arc> = match &level_config.chunk { - ChunkConfig::Linear(chunk_config) => Arc::new(ChunkFileManager::< - LinearFile, - >::new(chunk_config.clone())), - ChunkConfig::Anvil(chunk_config) => Arc::new(ChunkFileManager::< + ChunkConfig::Linear(config) => Arc::new( + ChunkFileManager::>::new(config.clone()), + ), + ChunkConfig::Anvil(config) => Arc::new(ChunkFileManager::< AnvilChunkFile, - >::new(chunk_config.clone())), + >::new(config.clone())), }; let (gen_entity_request_tx, gen_entity_request_rx) = crossbeam::channel::unbounded(); let pending_entity_generations = Arc::new(DashMap::new()); - let level_channel = Arc::new(LevelChannel::new()); let thread_tracker = Mutex::new(Vec::new()); let listener = Arc::new(ChunkListener::new()); @@ -214,8 +208,6 @@ impl Level { level_ref.thread_tracker.lock().unwrap().as_mut(), ); - // let mut tracker = level_ref.thread_tracker.lock().unwrap(); - // Entity Chunks for thread_id in 0..(num_threads / 2).max(1) { let level_clone = level_ref.clone(); let pending_clone = pending_entity_generations.clone(); @@ -223,7 +215,7 @@ impl Level { let builder = thread::Builder::new().name(format!("Entity Chunk Generation Thread {thread_id}")); - // tracker.push( TODO + builder .spawn(move || { while let Ok(pos) = rx.recv() { @@ -231,11 +223,6 @@ impl Level { break; } - // log::debug!( - // "Generating entity chunk {pos:?}, worker thread {thread_id:?}, queue length {}", - // rx.len() - // ); - let chunk = ChunkEntityData { x: pos.x, z: pos.y, @@ -248,25 +235,15 @@ impl Level { .loaded_entity_chunks .insert(pos, arc_chunk.clone()); - if let Some(waiters) = pending_clone.remove(&pos) { - for tx in waiters.1 { + if let Some((_, waiters)) = pending_clone.remove(&pos) { + for tx in waiters { let _ = tx.send(arc_chunk.clone()); } } } }) .unwrap(); - // ); } - // drop(tracker); - // level_ref - // .chunk_loading - // .lock() - // .unwrap() - // .add_ticket( - // Vector2::::new(0, 0), - // ChunkLoading::FULL_CHUNK_LEVEL - 1, - // ); level_ref } @@ -385,21 +362,16 @@ impl Level { /// before pub async fn mark_chunks_as_newly_watched(&self, chunks: &[Vector2]) { for chunk in chunks { - log::trace!("{chunk:?} marked as newly watched"); - match self.chunk_watchers.entry(*chunk) { - Entry::Occupied(mut occupied) => { - let value = occupied.get_mut(); + self.chunk_watchers + .entry(*chunk) + .and_modify(|value| { if let Some(new_value) = value.checked_add(1) { *value = new_value; - //log::debug!("Watch value for {:?}: {}", chunk, value); } else { - log::error!("Watching overflow on chunk {chunk:?}"); + log::error!("Watching overflow on chunk {:?}", chunk); } - } - Entry::Vacant(vacant) => { - vacant.insert(1); - } - } + }) + .or_insert(1); } // self.chunk_saver @@ -416,22 +388,14 @@ impl Level { let mut chunks_to_clean = Vec::new(); for chunk in chunks { - log::trace!("{chunk:?} marked as no longer watched"); - match self.chunk_watchers.entry(*chunk) { - Entry::Occupied(mut occupied) => { - let value = occupied.get_mut(); - *value = value.saturating_sub(1); + if let Some(mut count) = self.chunk_watchers.get_mut(chunk) { + let value = count.value_mut(); + *value = value.saturating_sub(1); - if *value == 0 { - occupied.remove_entry(); - chunks_to_clean.push(*chunk); - } - } - Entry::Vacant(_) => { - // This can be: - // - Player disconnecting before all packets have been sent - // - Player moving so fast that the chunk leaves the render distance before it - // is loaded into memory + if *value == 0 { + drop(count); + self.chunk_watchers.remove(chunk); + chunks_to_clean.push(*chunk); } } } @@ -506,13 +470,10 @@ impl Level { .iter() .map(|x| x.value().clone()) .collect::>(); - for chunk in chunks { - let mut chunk = chunk.write().await; - ticks.block_ticks.append(&mut chunk.block_ticks.step_tick()); - ticks.fluid_ticks.append(&mut chunk.fluid_ticks.step_tick()); - - let chunk = chunk.downgrade(); - + for chunk_sync in chunks { + // Try to get a READ lock first. + // Most chunks won't have pending ticks, so this is very fast. + let chunk = chunk_sync.read().await; let chunk_x_base = chunk.x * 16; let chunk_z_base = chunk.z * 16; @@ -544,21 +505,38 @@ impl Level { } for section_data in section_blocks { - for (random_pos, block_state_id) in section_data { - if has_random_ticks(block_state_id) { - ticks.random_ticks.push(ScheduledTick { - position: random_pos, - delay: 0, - priority: TickPriority::Normal, - value: (), - }); - } - } + ticks + .random_ticks + .extend( + section_data + .into_iter() + .filter_map(|(random_pos, block_state_id)| { + if has_random_ticks(block_state_id) { + Some(ScheduledTick { + position: random_pos, + delay: 0, + priority: TickPriority::Normal, + value: (), + }) + } else { + None + } + }), + ); } ticks .block_entities .extend(chunk.block_entities.values().cloned()); + + drop(chunk); + let mut chunk_write = chunk_sync.write().await; + ticks + .block_ticks + .append(&mut chunk_write.block_ticks.step_tick()); + ticks + .fluid_ticks + .append(&mut chunk_write.fluid_ticks.step_tick()); } ticks.block_ticks.sort_unstable(); @@ -603,8 +581,6 @@ impl Level { return chunk.clone(); } - log::debug!("Missing Chunk {pos:?}. Fetching."); - let clock = Instant::now(); let recv = self.chunk_listener.add_single_chunk_listener(pos); { @@ -613,16 +589,12 @@ impl Level { lock.send_change(); } - let ret = if let Some(chunk) = self.loaded_chunks.get(&pos) { + if let Some(chunk) = self.loaded_chunks.get(&pos) { chunk.clone() } else { recv.await .expect("Chunk listener dropped without sending chunk") - }; - - log::debug!("Chunk {pos:?} received after {:?}.", Instant::now() - clock); - - ret + } } async fn load_single_entity_chunk( @@ -846,6 +818,7 @@ impl Level { priority: TickPriority, ) { let chunk = self.get_chunk(block_pos.chunk_position()).await; + let tick_order = self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed); let mut chunk = chunk.write().await; chunk.block_ticks.schedule_tick( &ScheduledTick { @@ -854,9 +827,8 @@ impl Level { priority, value: unsafe { &*(block as *const Block) }, }, - self.schedule_tick_counts.load(Ordering::Relaxed), + tick_order, ); - self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed); } pub async fn schedule_fluid_tick( @@ -867,6 +839,7 @@ impl Level { priority: TickPriority, ) { let chunk = self.get_chunk(block_pos.chunk_position()).await; + let tick_order = self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed); let mut chunk = chunk.write().await; chunk.fluid_ticks.schedule_tick( &ScheduledTick { @@ -875,9 +848,8 @@ impl Level { priority, value: unsafe { &*(fluid as *const Fluid) }, }, - self.schedule_tick_counts.load(Ordering::Relaxed), + tick_order, ); - self.schedule_tick_counts.fetch_add(1, Ordering::Relaxed); } pub async fn is_block_tick_scheduled( diff --git a/pumpkin/src/block/blocks/carpet.rs b/pumpkin/src/block/blocks/carpet.rs index 85952ae4a..b72ee1b24 100644 --- a/pumpkin/src/block/blocks/carpet.rs +++ b/pumpkin/src/block/blocks/carpet.rs @@ -1,6 +1,7 @@ use crate::block::{ BlockBehaviour, BlockFuture, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnScheduledTickArgs, }; +use pumpkin_data::block_properties::is_air; use pumpkin_data::tag::{RegistryKey, get_tag_values}; use pumpkin_macros::{pumpkin_block, pumpkin_block_from_tag}; use pumpkin_util::math::position::BlockPos; @@ -108,8 +109,5 @@ impl BlockBehaviour for PaleMossCarpetBlock { } async fn can_place_at(block_accessor: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { - !block_accessor - .get_block_state(&block_pos.down()) - .await - .is_air() + !is_air(block_accessor.get_block_state_id(&block_pos.down()).await) } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 31057c690..e09f259e4 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -206,23 +206,6 @@ impl ChunkManager { let view_distance_i32 = i32::from(view_distance); - let mut chunks_to_watch = Vec::new(); - let mut chunks_to_unwatch = Vec::new(); - - for pos in &self.chunk_sent { - if (pos.x - center.x).abs().max((pos.y - center.y).abs()) > view_distance_i32 { - chunks_to_unwatch.push(*pos); - } - } - - let level_clone = level.clone(); - let chunks_to_unwatch_clone = chunks_to_unwatch.clone(); - tokio::spawn(async move { - level_clone - .mark_chunks_as_not_watched(&chunks_to_unwatch_clone) - .await; - }); - self.chunk_sent.retain(|pos| { (pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_distance_i32 }); @@ -242,20 +225,13 @@ impl ChunkManager { for dx in (-view_distance_i32)..=view_distance_i32 { for dy in (-view_distance_i32)..=view_distance_i32 { let new_pos = center.add_raw(dx, dy); - if !self.chunk_sent.contains(&new_pos) { - chunks_to_watch.push(new_pos); - if let Some(chunk) = level.loaded_chunks.get(&new_pos) { - self.push_chunk(new_pos, chunk.value().clone()); - } + if !self.chunk_sent.contains(&new_pos) + && let Some(chunk) = level.loaded_chunks.get(&new_pos) + { + self.push_chunk(new_pos, chunk.value().clone()); } } } - let level_clone = level.clone(); - tokio::spawn(async move { - level_clone - .mark_chunks_as_newly_watched(&chunks_to_watch) - .await; - }); } pub fn clean_up(&mut self, level: &Arc) { diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs index d81e8037c..d5387c0d0 100644 --- a/pumpkin/src/lib.rs +++ b/pumpkin/src/lib.rs @@ -20,7 +20,7 @@ use rustyline::Editor; use rustyline::history::FileHistory; use rustyline::{Config, error::ReadlineError}; use std::collections::HashMap; -use std::io::{Cursor, IsTerminal, stdin}; +use std::io::{Cursor, ErrorKind, IsTerminal, stdin}; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; @@ -222,10 +222,36 @@ impl PumpkinServer { let mut tcp_listener = None; if server.basic_config.java_edition { + let address = server.basic_config.java_edition_address; // Setup the TCP server socket. - let listener = tokio::net::TcpListener::bind(server.basic_config.java_edition_address) - .await - .expect("Failed to start `TcpListener`"); + let listener = match TcpListener::bind(address).await { + Ok(l) => l, + Err(e) => match e.kind() { + ErrorKind::AddrInUse => { + log::error!("Error: Address {} is already in use.", address); + log::error!( + "Make sure another instance of the server isn't already running" + ); + std::process::exit(1); + } + ErrorKind::PermissionDenied => { + log::error!("Error: Permission denied when binding to {}.", address); + log::error!("You might need sudo/admin privileges to use ports below 1024"); + std::process::exit(1); + } + ErrorKind::AddrNotAvailable => { + log::error!( + "Error: The address {} is not available on this machine", + address + ); + std::process::exit(1); + } + _ => { + log::error!("Failed to start TcpListener on {}: {}", address, e); + std::process::exit(1); + } + }, + }; // In the event the user puts 0 for their port, this will allow us to know what port it is running on let addr = listener .local_addr() diff --git a/pumpkin/src/net/java/login.rs b/pumpkin/src/net/java/login.rs index 4ee50fccc..1ea4fba39 100644 --- a/pumpkin/src/net/java/login.rs +++ b/pumpkin/src/net/java/login.rs @@ -90,7 +90,9 @@ impl JavaClient { let verify_token: [u8; 4] = rand::random(); // Wait until we have sent the encryption packet to the client self.send_packet_now( - &server.encryption_request(&verify_token, server.basic_config.online_mode), + &server + .encryption_request(&verify_token, server.basic_config.online_mode) + .await, ) .await; } else { @@ -107,7 +109,10 @@ impl JavaClient { encryption_response: SEncryptionResponse, ) { log::debug!("Handling encryption"); - let shared_secret = server.decrypt(&encryption_response.shared_secret).unwrap(); + let shared_secret = server + .decrypt(&encryption_response.shared_secret) + .await + .unwrap(); if let Err(error) = self.set_encryption(&shared_secret).await { self.kick(TextComponent::text(error.to_string())).await; @@ -207,7 +212,7 @@ impl JavaClient { shared_secret: &[u8], username: &str, ) -> Result { - let hash = server.digest_secret(shared_secret); + let hash = server.digest_secret(shared_secret).await; let ip = self.address.lock().await.ip(); let profile = authentication::authenticate( username, diff --git a/pumpkin/src/server/key_store.rs b/pumpkin/src/server/key_store.rs index 78909b32e..9a269b7c8 100644 --- a/pumpkin/src/server/key_store.rs +++ b/pumpkin/src/server/key_store.rs @@ -1,3 +1,5 @@ +use std::time::Instant; + use num_bigint::BigInt; use pkcs8::EncodePublicKey; use pumpkin_protocol::java::client::login::CEncryptionRequest; @@ -15,6 +17,7 @@ pub struct KeyStore { impl KeyStore { #[must_use] pub fn new() -> Self { + let instant = Instant::now(); log::debug!("Creating encryption keys..."); let private_key = Self::generate_private_key(); @@ -27,6 +30,8 @@ impl KeyStore { .to_vec() .into_boxed_slice(); + log::debug!("Created RSA keys, took {}ms", instant.elapsed().as_millis()); + Self { private_key, public_key_der, diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index ac78b618d..bfbee01a0 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -39,7 +39,7 @@ use std::net::IpAddr; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32}; use std::{future::Future, sync::atomic::Ordering, time::Duration}; -use tokio::sync::{Mutex, RwLock}; +use tokio::sync::{Mutex, OnceCell, RwLock}; use tokio::task::JoinHandle; use tokio_util::task::TaskTracker; @@ -59,7 +59,7 @@ pub struct Server { pub advanced_config: AdvancedConfiguration, /// Handles cryptographic keys for secure communication. - key_store: KeyStore, + key_store: OnceCell>, /// Manages server status information. listing: Mutex, /// Saves server branding information. @@ -190,7 +190,7 @@ impl Server { command_dispatcher, block_registry: block_registry.clone(), item_registry: super::item::items::default_registry(), - key_store: KeyStore::new(), + key_store: OnceCell::new(), listing, branding: CachedBranding::new(), bossbars: Mutex::new(CustomBossbars::new()), @@ -208,59 +208,86 @@ impl Server { level_info: level_info.clone(), _locker: Arc::new(locker), }; - let server = Arc::new(server); - let weak = Arc::downgrade(&server); - let level_config = &server.advanced_config.world; + let level_config = Arc::new(server.advanced_config.world.clone()); + + let server_clone = server.clone(); + tokio::spawn(async move { + server_clone + .key_store + .get_or_init(|| async { Arc::new(KeyStore::new()) }) + .await; + }); + + let weak_server = Arc::downgrade(&server); log::info!("Loading Overworld: {seed}"); - let overworld = World::load( - into_level( - Dimension::OVERWORLD, - level_config, - world_path.clone(), - block_registry.clone(), - seed, - ), - level_info.clone(), - Dimension::OVERWORLD, - block_registry.clone(), - weak.clone(), - ); - log::info!("Loading Nether: {seed}"); - let nether = World::load( - into_level( - Dimension::THE_NETHER, - level_config, - world_path.clone(), - block_registry.clone(), - seed, - ), - level_info.clone(), - Dimension::THE_NETHER, - block_registry.clone(), - weak.clone(), - ); - log::info!("Loading End: {seed}"); - let end = World::load( - into_level( - Dimension::THE_END, - level_config, - world_path, - block_registry.clone(), - seed, - ), - level_info, - Dimension::THE_END, - block_registry, - weak, - ); - *server - .worlds - .try_write() - .expect("Nothing should hold a lock of worlds before server startup") = - // vec![overworld.into()]; - vec![overworld.into(), nether.into(), end.into()]; + let overworld_task = tokio::task::spawn_blocking({ + let path = world_path.clone(); + let registry = block_registry.clone(); + let level_info = level_info.clone(); + let weak = weak_server.clone(); + let config = level_config.clone(); + move || { + World::load( + into_level(Dimension::OVERWORLD, &config, path, registry.clone(), seed), + level_info, + Dimension::OVERWORLD, + registry, + weak, + ) + } + }); + + let nether_task = tokio::task::spawn_blocking({ + let path = world_path.clone(); + let registry = block_registry.clone(); + let level_info = level_info.clone(); + let weak = weak_server.clone(); + let config = level_config.clone(); + move || { + World::load( + into_level(Dimension::THE_NETHER, &config, path, registry.clone(), seed), + level_info, + Dimension::THE_NETHER, + registry, + weak, + ) + } + }); + + let end_task = tokio::task::spawn_blocking({ + let path = world_path.clone(); + let registry = block_registry.clone(); + let level_info = level_info.clone(); + let weak = weak_server.clone(); + let config = level_config.clone(); + move || { + World::load( + into_level(Dimension::THE_END, &config, path, registry.clone(), seed), + level_info, + Dimension::THE_END, + registry, + weak, + ) + } + }); + + let (overworld_res, nether_res, end_res) = + tokio::join!(overworld_task, nether_task, end_task); + + let overworld = overworld_res.expect("Overworld load panicked"); + let nether = nether_res.expect("Nether load panicked"); + let end = end_res.expect("End load panicked"); + + { + let mut worlds = server.worlds.write().await; + worlds.push(overworld.into()); + worlds.push(nether.into()); + worlds.push(end.into()); + }; + + log::info!("All worlds loaded successfully."); server } @@ -623,21 +650,29 @@ impl Server { &self.listing } - pub fn encryption_request<'a>( + pub async fn encryption_request<'a>( &'a self, verification_token: &'a [u8; 4], should_authenticate: bool, ) -> CEncryptionRequest<'a> { self.key_store + .get_or_init(|| async { Arc::new(KeyStore::new()) }) + .await .encryption_request("", verification_token, should_authenticate) } - pub fn decrypt(&self, data: &[u8]) -> Result, EncryptionError> { - self.key_store.decrypt(data) + pub async fn decrypt(&self, data: &[u8]) -> Result, EncryptionError> { + self.key_store + .get_or_init(|| async { Arc::new(KeyStore::new()) }) + .await + .decrypt(data) } - pub fn digest_secret(&self, secret: &[u8]) -> String { - self.key_store.get_digest(secret) + pub async fn digest_secret(&self, secret: &[u8]) -> String { + self.key_store + .get_or_init(|| async { Arc::new(KeyStore::new()) }) + .await + .get_digest(secret) } /// Main server tick method. This now handles both player/network ticking (which always runs) @@ -711,6 +746,22 @@ impl Server { self.aggregated_tick_times_nanos.load(Ordering::Relaxed) / sample_size as i64 } + /// Returns the average Milliseconds Per Tick (MSPT). + pub fn get_mspt(&self) -> f64 { + let avg_nanos = self.get_average_tick_time_nanos(); + // Convert nanoseconds to decimal milliseconds + avg_nanos as f64 / 1_000_000.0 + } + + /// Returns the Ticks Per Second (TPS). + pub fn get_tps(&self) -> f64 { + let mspt = self.get_mspt(); + if mspt <= 0.0 { + return 0.0; + } + 1000.0 / mspt + } + /// Returns a copy of the last 100 tick times. pub async fn get_tick_times_nanos_copy(&self) -> [i64; 100] { *self.tick_times_nanos.lock().await diff --git a/pumpkin/src/world/chunker.rs b/pumpkin/src/world/chunker.rs index 8c1535002..05bd844e2 100644 --- a/pumpkin/src/world/chunker.rs +++ b/pumpkin/src/world/chunker.rs @@ -22,74 +22,77 @@ pub async fn get_view_distance(player: &Player) -> NonZeroU8 { pub async fn update_position(player: &Arc) { let entity = &player.living_entity.entity; + let new_chunk_center = entity.chunk_pos.load(); + let old_cylindrical = player.watched_section.load(); + + if old_cylindrical.center == new_chunk_center { + return; + } let view_distance = get_view_distance(player).await; - let new_chunk_center = entity.chunk_pos.load(); - - let old_cylindrical = player.watched_section.load(); let new_cylindrical = Cylindrical::new(new_chunk_center, view_distance); - if old_cylindrical != new_cylindrical { - match &player.client { - ClientPlatform::Java(java_client) => { - java_client - .send_packet_now(&CCenterChunk { - chunk_x: new_chunk_center.x.into(), - chunk_z: new_chunk_center.y.into(), - }) - .await; - } - ClientPlatform::Bedrock(bedrock_client) => { - bedrock_client - .send_game_packet(&CNetworkChunkPublisherUpdate::new( - player.get_entity().block_pos.load(), - u32::from(view_distance.get()) * 16, - )) - .await; - } + if old_cylindrical == new_cylindrical { + return; + } + + match &player.client { + ClientPlatform::Java(java_client) => { + java_client + .send_packet_now(&CCenterChunk { + chunk_x: new_chunk_center.x.into(), + chunk_z: new_chunk_center.y.into(), + }) + .await; } - let mut loading_chunks = Vec::new(); - let mut unloading_chunks = Vec::new(); - Cylindrical::for_each_changed_chunk( - old_cylindrical, - new_cylindrical, - &mut loading_chunks, - &mut unloading_chunks, - ); - - // Make sure the watched section and the chunk watcher updates are async atomic. We want to - // ensure what we unload when the player disconnects is correct. - let level = &entity.world.level; - level.mark_chunks_as_newly_watched(&loading_chunks).await; - let chunks_to_clean = level.mark_chunks_as_not_watched(&unloading_chunks).await; - - { - let mut chunk_manager = player.chunk_manager.lock().await; - chunk_manager.update_center_and_view_distance( - new_chunk_center, - view_distance.into(), - level, - ); - }; - - player.watched_section.store(new_cylindrical); - - if !chunks_to_clean.is_empty() { - // level.clean_chunks(&chunks_to_clean).await; - for chunk in unloading_chunks { - player - .client - .enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) - .await; - } - } - - if !loading_chunks.is_empty() { - entity.world.spawn_world_entity_chunks( - player.clone(), - loading_chunks, - new_chunk_center, - ); + ClientPlatform::Bedrock(bedrock_client) => { + bedrock_client + .send_game_packet(&CNetworkChunkPublisherUpdate::new( + player.get_entity().block_pos.load(), + u32::from(view_distance.get()) * 16, + )) + .await; } } + let mut loading_chunks = Vec::new(); + let mut unloading_chunks = Vec::new(); + Cylindrical::for_each_changed_chunk( + old_cylindrical, + new_cylindrical, + &mut loading_chunks, + &mut unloading_chunks, + ); + + let level = &entity.world.level; + + { + let mut chunk_manager = player.chunk_manager.lock().await; + chunk_manager.update_center_and_view_distance( + new_chunk_center, + view_distance.into(), + level, + ); + }; + + player.watched_section.store(new_cylindrical); + + // Make sure the watched section and the chunk watcher updates are async atomic. We want to + // ensure what we unload when the player disconnects is correct. + level.mark_chunks_as_newly_watched(&loading_chunks).await; + level.mark_chunks_as_not_watched(&unloading_chunks).await; + + if let ClientPlatform::Java(_) = &player.client { + for chunk in &unloading_chunks { + player + .client + .enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) + .await; + } + } + + if !loading_chunks.is_empty() { + entity + .world + .spawn_world_entity_chunks(player.clone(), loading_chunks, new_chunk_center); + } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index f1253f733..34db0061a 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -38,6 +38,7 @@ use bytes::BufMut; use crossbeam::queue::SegQueue; use explosion::Explosion; use pumpkin_config::BasicConfiguration; +use pumpkin_data::block_properties::is_air; use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::MobCategory; @@ -1874,10 +1875,6 @@ impl World { chunks: Vec>, center_chunk: Vector2, ) { - if player.client.closed() { - log::info!("The connection has closed before world chunks were spawned"); - return; - } #[cfg(debug_assertions)] let inst = std::time::Instant::now(); @@ -3054,7 +3051,7 @@ impl World { if new_state_id != block_state_id { let flags = flags & !BlockFlags::SKIP_DROPS; - if BlockState::from_id(new_state_id).is_air() { + if is_air(new_state_id) { self.break_block(block_pos, None, flags).await; } else { self.set_block_state(block_pos, new_state_id, flags).await;