1mod broadcasting;
3pub mod jobs;
5mod packet_processor;
6mod pregen;
7pub mod registry_cache;
9mod run_loop;
10pub mod tick_rate_manager;
12mod world_tick_workers;
13pub mod worlds;
15
16use crate::bootstrap::init_globals;
17use crate::chunk::{
18 chunk_request::{ChunkRequest, ChunkRequestHandle, ChunkRequestState, ChunkTicketKind},
19 status::ChunkStatus,
20};
21use crate::command::brigadier::{StringReader, SuggestionError, Suggestions};
22use crate::command::execution::{
23 CommandExecutionContext, CommandResultCallback, CommandSource, ExecutionCommandSource,
24 ExecutionStop,
25};
26use crate::command::sender::{CommandExecutionOwner, CommandSender};
27use crate::command::storage::DomainCommandStorage;
28use crate::command::{
29 COMMAND_REQUESTS_PER_TICK, COMMAND_RESUMPTIONS_PER_TICK, CommandCompletion, CommandDispatcher,
30 CommandQueueFull, CommandRegistry, CommandRequest, CommandRequestQueue,
31 PendingCommandExecutionQueue, client_permission_event, command_suggestions_packet,
32 command_tree_packet, create_registered_dispatcher,
33};
34use crate::config::{ResolvedWorldConfig, RuntimeConfig, WorldsConfig, validate_login_security};
35use crate::entity::{
36 Entity, EntityBase, PendingWorldChangeToken, RemovalReason, SharedEntity, change_entity_world,
37};
38
39use crate::chunk_saver::{ChunkStorage, PersistentEntity, registry::WorldStorageRegistry};
40use crate::level_data::{LevelDataManager, RespawnData, WorldGenerationSettings};
41use crate::permission::{
42 OP_GROUP, PermissionGroupManager, PermissionGroupManagerError, PermissionGroupUpdateError,
43 PermissionGroupsConfig, PermissionMetadataExpression, PermissionRuleExpression, PermissionSet,
44 PermissionSubjectIndex, PermissionSubjectState,
45};
46use crate::player::chunk_sender::{ChunkSender, EncodedChunk};
47use crate::player::connection::NetworkConnection;
48use crate::player::connection::ScheduledPlayPacket;
49use crate::player::player_data::{
50 PersistentEnderPearl, PersistentPlayerData, PersistentRootVehicle,
51};
52use crate::player::player_data_storage::{GlobalPlayerData, PlayerDataStorage};
53use crate::player::player_inventory::MenuRemovalStatus;
54use crate::player::{
55 DomainResidenceToken, GameProfile, KnownPlayer, KnownPlayerNameLookup, KnownPlayers, Player,
56 ProfileLookupError, ResetReason, is_valid_player_name, lookup_online_profile, offline_uuid,
57};
58use crate::portal::{
59 PortalKind, TeleportPostTransition, TeleportTransition, WorldChangeRequest, end_gateway,
60 end_portal, nether_portal,
61};
62use crate::scoreboard::DomainScoreboards;
63use crate::server::jobs::{FnServerJob, ServerJobContext, ServerJobQueue};
64use crate::server::packet_processor::PacketProcessor;
65use crate::server::registry_cache::RegistryCache;
66use crate::server::worlds::WorldMap;
67use crate::world::player_spawn_finder::{PlayerSpawnSearch, PlayerSpawnSearchPoll};
68use crate::world::{PlayerMap, World, WorldConfig};
69use crate::worldgen::WorldGeneratorRegistry;
70use crate::worldgen::registry::GeneratorOutput;
71use crossbeam::queue::SegQueue;
72use glam::DVec3;
73use rayon::{ThreadPool, ThreadPoolBuilder};
74use rustc_hash::FxHashMap;
75use std::{
76 collections::BTreeSet,
77 io, mem,
78 num::NonZero,
79 path::Path,
80 sync::{Arc, mpsc},
81 thread,
82 time::{Duration, Instant},
83};
84use steel_crypto::key_store::KeyStore;
85use steel_protocol::packet_traits::{ClientPacket, EncodedPacket};
86use steel_protocol::packets::game::{
87 CCommandSuggestions, CEntityEvent, CGameEvent, CLogin, CPlayerInfoUpdate, CRemovePlayerInfo,
88 CSetDefaultSpawnPosition, CSystemChat, CTabList, CTickingState, CTickingStep,
89 CommonPlayerSpawnInfo, GameEventType, RelativeMovement,
90};
91use steel_protocol::utils::ConnectionProtocol;
92use steel_registry::vanilla_game_rules::{
93 ALLOW_ENTERING_NETHER_USING_PORTALS, IMMEDIATE_RESPAWN, LIMITED_CRAFTING, REDUCED_DEBUG_INFO,
94};
95use steel_registry::{
96 RegistryEntry, dimension_type::DimensionTypeRef, vanilla_dimension_types, vanilla_entities,
97};
98use steel_utils::{
99 BlockPos, ChunkPos, Identifier,
100 locks::{AsyncMutex, SyncMutex, SyncRwLock},
101 text::DisplayResolutor,
102 translations,
103};
104use text_components::{Modifier, TextComponent, format::Color};
105use tick_rate_manager::{SprintReport, TickRateManager};
106use tokio::{
107 runtime::Runtime,
108 sync::Notify,
109 task::{JoinSet, spawn_blocking},
110 time::sleep,
111};
112use tokio_util::sync::CancellationToken;
113use uuid::Uuid;
114
115const TAB_LIST_UPDATE_INTERVAL: u64 = 20;
117const SEND_PLAYER_INFO_INTERVAL: u64 = 600;
120const COMMAND_DATA_AUTOSAVE_INTERVAL: Duration = Duration::from_secs(300);
123
124#[derive(Clone, Copy)]
125struct TabListTickStats {
126 tps: f32,
127 recent_mspt: f32,
128 average_mspt: f32,
129 p95_mspt: f32,
130}
131
132impl TabListTickStats {
133 fn capture(tick_manager: &TickRateManager) -> Self {
134 Self {
135 tps: tick_manager.get_tps(),
136 recent_mspt: tick_manager.get_smoothed_mspt(),
137 average_mspt: tick_manager.get_average_mspt(),
138 p95_mspt: tick_manager.get_p95(),
139 }
140 }
141}
142
143pub struct CommandDataSaveResults {
145 pub scoreboards: io::Result<usize>,
147 pub storage: io::Result<usize>,
149}
150
151mod known_players;
152
153use known_players::KnownPlayerCacheState;
154
155const CHUNK_SENDING_TPS: u64 = 20;
157
158const SLOW_CHUNK_TICK_THRESHOLD: Duration = Duration::from_millis(50);
160
161fn configured_chunk_generation_threads(configured_threads: Option<usize>) -> Option<usize> {
162 cap_positive_thread_count(configured_threads, available_worker_threads())
163}
164
165fn configured_chunk_encoding_threads(configured_threads: Option<usize>) -> Option<usize> {
166 cap_positive_thread_count(configured_threads, available_worker_threads())
167}
168
169fn configured_packet_workers(configured_workers: Option<usize>) -> usize {
170 packet_workers_for_available(configured_workers, available_worker_threads())
171}
172
173fn available_worker_threads() -> usize {
174 thread::available_parallelism().map_or(4, NonZero::get)
175}
176
177fn cap_positive_thread_count(
178 configured_threads: Option<usize>,
179 available_threads: usize,
180) -> Option<usize> {
181 let configured_threads = configured_threads.filter(|&threads| threads > 0)?;
182 Some(configured_threads.min(available_threads.max(1)))
183}
184
185fn packet_workers_for_available(
186 configured_workers: Option<usize>,
187 available_threads: usize,
188) -> usize {
189 let available_threads = available_threads.max(1);
190 if let Some(configured_workers) = configured_workers.filter(|&workers| workers > 0) {
191 return configured_workers.min(available_threads);
192 }
193
194 ((available_threads / 2).max(2)).min(available_threads)
195}
196
197#[cfg(test)]
198mod tests;
199
200#[derive(Clone, Copy)]
201struct PreparedSpawn {
202 position: DVec3,
203 rotation: (f32, f32),
204}
205
206fn apply_default_spawn(player: &Arc<Player>, world: &Arc<World>, spawn: PreparedSpawn) {
207 player.base().set_position_local(spawn.position);
208 player.set_rotation(spawn.rotation);
209 player.restore_game_modes(world.default_gamemode, None);
210 player
211 .abilities
212 .lock()
213 .update_for_game_mode(world.default_gamemode);
214}
215
216fn is_allowed_to_enter_portal(source_world: &World, target_world: &World) -> bool {
217 is_allowed_to_enter_portal_target(
218 is_nether_dimension_type(target_world),
219 source_world.get_game_rule(&ALLOW_ENTERING_NETHER_USING_PORTALS),
220 )
221}
222
223const fn is_allowed_to_enter_portal_target(
224 target_is_nether: bool,
225 allow_entering_nether_using_portals: bool,
226) -> bool {
227 if !target_is_nether {
228 return true;
229 }
230
231 allow_entering_nether_using_portals
232}
233
234fn can_teleport_between_worlds(
235 entity: &dyn Entity,
236 source_world: &World,
237 target_world: &World,
238 projectile_owner_seen_credits: impl Fn(&uuid::Uuid) -> Option<bool>,
239) -> bool {
240 if is_end_return_transition(source_world.dimension_type, target_world.dimension_type) {
241 return can_entity_return_from_end_to_overworld(entity, projectile_owner_seen_credits);
242 }
243
244 true
245}
246
247fn is_end_return_transition(
248 source_dimension_type: DimensionTypeRef,
249 target_dimension_type: DimensionTypeRef,
250) -> bool {
251 source_dimension_type == &vanilla_dimension_types::THE_END
252 && target_dimension_type == &vanilla_dimension_types::OVERWORLD
253}
254
255fn is_nether_dimension_type(world: &World) -> bool {
256 world.dimension_type == &vanilla_dimension_types::THE_NETHER
257}
258
259fn is_end_dimension_type(world: &World) -> bool {
260 world.dimension_type == &vanilla_dimension_types::THE_END
261}
262
263fn can_entity_return_from_end_to_overworld(
264 entity: &dyn Entity,
265 projectile_owner_seen_credits: impl Fn(&uuid::Uuid) -> Option<bool>,
266) -> bool {
267 if entity.entity_type() == &vanilla_entities::ENDER_PEARL
268 && entity
269 .projectile_owner_uuid()
270 .and_then(|uuid| projectile_owner_seen_credits(&uuid))
271 == Some(false)
272 {
273 return false;
274 }
275
276 direct_passengers_allow_end_return(entity)
277}
278
279fn direct_passengers_allow_end_return(entity: &dyn Entity) -> bool {
280 for passenger in entity.passengers() {
281 if passenger
282 .as_player()
283 .is_some_and(|player| !player.has_seen_credits())
284 {
285 return false;
286 }
287 }
288
289 true
290}
291
292fn local_respawn_data_for_world(world: &World) -> RespawnData {
293 let level_data = world.level_data.read();
294 let data = level_data.data();
295 RespawnData::of(world.key.clone(), data.spawn_pos(), data.spawn.angle, 0.0)
296}
297
298fn generation_settings_for_world(
299 world_entry: &ResolvedWorldConfig,
300 generator_output: &GeneratorOutput,
301) -> WorldGenerationSettings {
302 WorldGenerationSettings::from_generator_config(
303 world_entry.generator_config.generator().clone(),
304 &generator_output.config,
305 generator_output.dimension_type.key.clone(),
306 generator_output.dimension_type.min_y,
307 generator_output.dimension_type.height,
308 )
309}
310
311fn world_config_registries() -> Result<(WorldGeneratorRegistry, WorldStorageRegistry), String> {
312 let generator_registry = WorldGeneratorRegistry::new_with_builtins()
313 .map_err(|e| format!("failed to initialize world generator registry: {e}"))?;
314 let storage_registry = WorldStorageRegistry::new_with_builtins()
315 .map_err(|e| format!("failed to initialize world storage registry: {e}"))?;
316 Ok((generator_registry, storage_registry))
317}
318
319struct DomainPlayerState {
320 world: Arc<World>,
321 data: DomainPlayerData,
322 spawn_chunk_request: ChunkRequestHandle,
323}
324
325struct UnpreparedDomainPlayerState {
326 world: Arc<World>,
327 explicit_target: bool,
328 data: UnpreparedDomainPlayerData,
329}
330
331enum UnpreparedDomainPlayerData {
332 SavedRestored { data: Box<PersistentPlayerData> },
333 SavedWithoutLocation { data: Box<PersistentPlayerData> },
334 FirstVisit,
335}
336
337enum DomainPlayerData {
338 SavedRestored {
339 data: Box<PersistentPlayerData>,
340 },
341 SavedWithoutLocation {
342 data: Box<PersistentPlayerData>,
343 spawn: PreparedSpawn,
344 },
345 FirstVisit {
346 spawn: PreparedSpawn,
347 },
348}
349
350struct DomainSwitchRequest {
351 player: Arc<Player>,
352 target_domain: String,
353 target_world: Option<Arc<World>>,
354 pending_token: PendingWorldChangeToken,
355}
356
357#[derive(Debug, thiserror::Error)]
359pub enum PlayerPermissionUpdateError<E> {
360 #[error("{0}")]
362 Edit(E),
363 #[error("unknown permission group '{0}'")]
365 UnknownGroup(String),
366 #[error("failed to update player permissions: {0}")]
368 Storage(io::Error),
369}
370
371impl<E> From<io::Error> for PlayerPermissionUpdateError<E> {
372 fn from(value: io::Error) -> Self {
373 Self::Storage(value)
374 }
375}
376
377mod permissions;
378
379#[cfg(test)]
380use permissions::validate_player_permission_group_update;
381
382mod player_admission;
383mod player_lifecycle;
384
385use player_admission::{PlayerAdmissionState, PlayerDisconnectQueue, PlayerJoinQueue};
386
387mod world_changes;
388
389use jobs::domain_switch::DomainSwitchJob;
390use jobs::teleport::{
391 EndGatewayTeleportJob, EndPortalTeleportJob, EnderPearlRestoreJob, NetherPortalTeleportJob,
392 RootVehicleRestoreJob, WorldSpawnTeleportJob, clear_pending_world_change,
393 portal_entity_still_valid,
394};
395
396pub struct Server {
398 pub config: Arc<RuntimeConfig>,
400 pub permission_groups: PermissionGroupManager,
402 pub cancel_token: CancellationToken,
404 pub key_store: KeyStore,
406 pub registry_cache: RegistryCache,
408 pub worlds: WorldMap,
410 online_players: PlayerMap,
412 player_admissions: SyncMutex<FxHashMap<Uuid, PlayerAdmissionState>>,
414 pub tick_rate_manager: SyncRwLock<TickRateManager>,
416 pub scoreboards: DomainScoreboards,
418 pub(crate) command_storage: DomainCommandStorage,
420 command_dispatcher: SyncRwLock<CommandDispatcher>,
422 command_permission_keys: Vec<String>,
424 command_requests: CommandRequestQueue,
426 packet_processor: PacketProcessor,
428 chunk_encoding_pool: Arc<ThreadPool>,
430 pub jobs: ServerJobQueue,
432 pub player_data_storage: PlayerDataStorage,
434 player_permission_states: SyncRwLock<PermissionSubjectIndex>,
436 player_permission_updates: AsyncMutex<()>,
438 known_players: SyncMutex<KnownPlayerCacheState>,
440 known_player_save_idle: Notify,
442 profile_lookup_client: reqwest::Client,
444 pending_player_joins: PlayerJoinQueue,
446 pending_player_disconnects: PlayerDisconnectQueue,
448 pub pending_world_changes: SyncMutex<Vec<(SharedEntity, WorldChangeRequest)>>,
450 pending_domain_switches: SyncMutex<Vec<DomainSwitchRequest>>,
452}
453
454struct GameTickTaskGuard {
455 server: Arc<Server>,
456 cancel_token: CancellationToken,
457}
458
459impl GameTickTaskGuard {
460 const fn new(server: Arc<Server>, cancel_token: CancellationToken) -> Self {
461 Self {
462 server,
463 cancel_token,
464 }
465 }
466}
467
468impl Drop for GameTickTaskGuard {
469 fn drop(&mut self) {
470 self.server.packet_processor.stop();
471 self.cancel_token.cancel();
472 }
473}
474
475impl Server {
476 pub(crate) fn permission_rule_suggestions(&self) -> Vec<String> {
477 let mut suggestions = self
478 .command_permission_keys
479 .iter()
480 .cloned()
481 .collect::<BTreeSet<_>>();
482 let config = self.permission_groups.config_snapshot();
483 for group in config.groups.values() {
484 suggestions.extend(group.allow.iter().cloned());
485 suggestions.extend(group.deny.iter().cloned());
486 }
487 for (_, state) in self.player_permission_states.read().entries() {
488 suggestions.extend(state.overrides().entries().iter().map(|entry| {
489 PermissionRuleExpression::new(entry.key().clone(), entry.context().clone())
490 .to_string()
491 }));
492 }
493 suggestions.into_iter().collect()
494 }
495
496 pub(crate) fn permission_metadata_suggestions(&self) -> Vec<String> {
497 let mut suggestions = BTreeSet::new();
498 let config = self.permission_groups.config_snapshot();
499 for group in config.groups.values() {
500 suggestions.extend(group.metadata.iter().map(|rule| rule.key.clone()));
501 }
502 for (_, state) in self.player_permission_states.read().entries() {
503 suggestions.extend(state.metadata_overrides().entries().iter().map(|entry| {
504 PermissionMetadataExpression::new(entry.key().clone(), entry.context().clone())
505 .to_string()
506 }));
507 }
508 suggestions.into_iter().collect()
509 }
510
511 pub async fn new(
513 chunk_runtime: Arc<Runtime>,
514 cancel_token: CancellationToken,
515 config: RuntimeConfig,
516 worlds_config: WorldsConfig,
517 permission_groups: PermissionGroupManager,
518 ) -> Result<Self, String> {
519 Self::new_with_commands(
520 chunk_runtime,
521 cancel_token,
522 config,
523 worlds_config,
524 permission_groups,
525 CommandRegistry::new(),
526 )
527 .await
528 }
529
530 #[expect(
532 clippy::too_many_lines,
533 reason = "server initialization is a single cohesive flow"
534 )]
535 pub async fn new_with_commands(
536 chunk_runtime: Arc<Runtime>,
537 cancel_token: CancellationToken,
538 config: RuntimeConfig,
539 worlds_config: WorldsConfig,
540 permission_groups: PermissionGroupManager,
541 command_registry: CommandRegistry,
542 ) -> Result<Self, String> {
543 validate_login_security(config.online_mode, config.encryption).map_err(str::to_owned)?;
544 let config = Arc::new(config);
545 init_globals()?;
546 log::info!(
547 "SteelMC is not affiliated with Mojang or Microsoft. Use is subject to the Minecraft EULA: https://aka.ms/MinecraftEULA"
548 );
549
550 let registry_cache = RegistryCache::new(config.compression);
551
552 let (generator_registry, storage_registry) = world_config_registries()?;
553 let resolved_worlds = worlds_config
554 .validate_and_resolve(&generator_registry, &storage_registry)
555 .map_err(|e| format!("failed to validate worlds.toml: {e}"))?;
556
557 let generation_pool: Arc<ThreadPool> = Arc::new({
558 let mut builder = ThreadPoolBuilder::new().thread_name(|i| format!("rayon-gen-{i}"));
559 if let Some(chunk_generation_threads) =
560 configured_chunk_generation_threads(config.chunk_generation_threads)
561 {
562 builder = builder.num_threads(chunk_generation_threads);
563 }
564 if cfg!(debug_assertions) {
566 builder = builder.stack_size(8 * 1024 * 1024);
567 }
568 builder
569 .build()
570 .map_err(|e| format!("failed to create generation thread pool: {e}"))?
571 });
572 let chunk_encoding_pool = Arc::new({
573 let mut builder =
574 ThreadPoolBuilder::new().thread_name(|i| format!("rayon-chunk-encode-{i}"));
575 if let Some(chunk_encoding_threads) =
576 configured_chunk_encoding_threads(config.chunk_encoding_threads)
577 {
578 builder = builder.num_threads(chunk_encoding_threads);
579 }
580 builder
581 .build()
582 .map_err(|e| format!("failed to create chunk encoding thread pool: {e}"))?
583 });
584
585 let player_data_storage = PlayerDataStorage::new(
586 resolved_worlds.save_path.clone(),
587 resolved_worlds.player_storage.clone(),
588 )
589 .await
590 .map_err(|e| format!("failed to create player data storage: {e}"))?;
591 let player_permission_states = player_data_storage
592 .load_permission_subjects()
593 .await
594 .map_err(|error| format!("failed to load player permissions: {error}"))?;
595 let known_players = player_data_storage
596 .load_known_players()
597 .await
598 .map_err(|error| format!("failed to load known players: {error}"))?;
599 let mut worlds = WorldMap::new(
600 resolved_worlds.default_domain.clone(),
601 &resolved_worlds.domains,
602 &resolved_worlds.worlds,
603 );
604
605 for world_entry in &resolved_worlds.worlds {
606 let default_world_path = resolved_worlds
607 .save_path
608 .join(&world_entry.domain)
609 .join("worlds")
610 .join(&world_entry.name);
611 let storage_output = storage_registry
612 .create(
613 &world_entry.storage,
614 &resolved_worlds.save_path,
615 Path::new(&default_world_path),
616 )
617 .map_err(|e| format!("failed to create storage for {}: {e}", world_entry.key))?;
618 let world_seed = LevelDataManager::load_seed_or_default(
619 storage_output.level_data_path.as_deref(),
620 world_entry.seed,
621 )
622 .await
623 .map_err(|e| {
624 format!(
625 "failed to load level data seed for {}: {e}",
626 world_entry.key
627 )
628 })?;
629 let generator_output = generator_registry
630 .create(
631 storage_output.level_data_path.as_deref(),
632 &world_entry.generator_config,
633 world_seed,
634 generation_pool.clone(),
635 )
636 .map_err(|e| format!("failed to create generator for {}: {e}", world_entry.key))?;
637 let generation_settings = generation_settings_for_world(world_entry, &generator_output);
638 let world = World::new_with_config_and_encoding_pool(
639 chunk_runtime.clone(),
640 world_entry.key.clone(),
641 generator_output.dimension_type,
642 world_seed,
643 WorldConfig {
644 storage: storage_output.storage,
645 level_data_path: storage_output
646 .level_data_path
647 .map(|path| path.to_string_lossy().into_owned()),
648 generator: Arc::new(generator_output.generator),
649 generation_settings,
650 view_distance: config.view_distance,
651 simulation_distance: config.simulation_distance,
652 max_chained_neighbor_updates: config.max_chained_neighbor_updates,
653 compression: config.compression,
654 is_flat: generator_output.is_flat,
655 sea_level: generator_output.sea_level,
656 default_gamemode: world_entry.default_gamemode,
657 difficulty: world_entry.difficulty,
658 },
659 generation_pool.clone(),
660 Arc::clone(&chunk_encoding_pool),
661 )
662 .await
663 .map_err(|e| format!("failed to create world {}: {e}", world_entry.key))?;
664 world
665 .initialize_spawn_if_needed()
666 .await
667 .map_err(|e| format!("failed to initialize spawn for {}: {e}", world_entry.key))?;
668 worlds.insert(world_entry.key.clone(), world);
669 }
670
671 let scoreboards = DomainScoreboards::load(&worlds)
672 .await
673 .map_err(|error| format!("failed to load domain scoreboards: {error}"))?;
674 let command_storage = DomainCommandStorage::load(&worlds)
675 .await
676 .map_err(|error| format!("failed to load domain command storage: {error}"))?;
677 let registered_commands = create_registered_dispatcher(command_registry)
678 .map_err(|error| format!("failed to register commands: {error}"))?;
679 let command_permission_keys = registered_commands
680 .permissions
681 .into_iter()
682 .map(|permission| permission.as_str().to_owned())
683 .collect();
684
685 Ok(Server {
686 config,
687 permission_groups,
688 cancel_token,
689 key_store: KeyStore::create(),
690 worlds,
691 online_players: PlayerMap::new(),
692 player_admissions: SyncMutex::new(FxHashMap::default()),
693 registry_cache,
694 tick_rate_manager: SyncRwLock::new(TickRateManager::new()),
695 scoreboards,
696 command_storage,
697 command_dispatcher: SyncRwLock::new(registered_commands.dispatcher),
698 command_permission_keys,
699 command_requests: CommandRequestQueue::new(),
700 packet_processor: PacketProcessor::new(),
701 chunk_encoding_pool,
702 jobs: ServerJobQueue::new(),
703 player_data_storage,
704 player_permission_states: SyncRwLock::new(player_permission_states),
705 player_permission_updates: AsyncMutex::new(()),
706 known_players: SyncMutex::new(KnownPlayerCacheState::new(known_players)),
707 known_player_save_idle: Notify::new(),
708 profile_lookup_client: reqwest::Client::new(),
709 pending_player_joins: PlayerJoinQueue::new(),
710 pending_player_disconnects: PlayerDisconnectQueue::new(),
711 pending_world_changes: SyncMutex::new(vec![]),
712 pending_domain_switches: SyncMutex::new(vec![]),
713 })
714 }
715
716 pub async fn save_command_storage(&self) -> io::Result<usize> {
718 self.command_storage.save(&self.worlds).await
719 }
720
721 pub async fn save_command_data(&self) -> CommandDataSaveResults {
723 CommandDataSaveResults {
724 scoreboards: self.scoreboards.save(&self.worlds).await,
725 storage: self.save_command_storage().await,
726 }
727 }
728
729 pub fn submit_command(
731 &self,
732 sender: CommandSender,
733 command: String,
734 ) -> Result<(), CommandQueueFull> {
735 self.command_requests.submit(CommandRequest::Execute {
736 owner: CommandExecutionOwner::capture(sender, self),
737 command,
738 })
739 }
740
741 pub(crate) fn submit_command_suggestions(
742 &self,
743 player: Arc<Player>,
744 transaction_id: i32,
745 input: String,
746 ) -> Result<(), CommandQueueFull> {
747 self.command_requests.submit(CommandRequest::Suggestions {
748 owner: CommandExecutionOwner::capture(CommandSender::Player(player), self),
749 transaction_id,
750 input,
751 })
752 }
753
754 pub(crate) fn schedule_play_packet(
756 &self,
757 player: Arc<Player>,
758 packet: ScheduledPlayPacket,
759 payload_bytes: usize,
760 ) {
761 self.packet_processor
762 .schedule(player, packet, payload_bytes);
763 }
764
765 pub fn command_completions(
767 self: &Arc<Self>,
768 sender: CommandSender,
769 input: &str,
770 ) -> Vec<CommandCompletion> {
771 if !CommandExecutionOwner::capture(sender.clone(), self).is_current(self) {
772 return Vec::new();
773 }
774 match self.build_command_suggestions(sender, input) {
775 Ok(suggestions) => {
776 let range = suggestions.range();
777 suggestions
778 .list()
779 .iter()
780 .map(|suggestion| {
781 CommandCompletion::new(
782 range.start(),
783 range.len(),
784 suggestion.text().to_owned(),
785 )
786 })
787 .collect()
788 }
789 Err(error) => {
790 tracing::warn!(%error, "failed to build command suggestions");
791 Vec::new()
792 }
793 }
794 }
795}