1mod broadcasting;
3pub mod jobs;
5mod packet_processor;
6mod pregen;
7pub mod registry_cache;
9mod run_loop;
10mod service_keys;
11mod tick_overload;
12pub mod tick_rate_manager;
14mod world_tick_workers;
15pub mod worlds;
17
18use crate::bootstrap::init_globals;
19use crate::chunk::{
20 chunk_request::{ChunkRequest, ChunkRequestHandle, ChunkRequestState, ChunkTicketKind},
21 status::ChunkStatus,
22};
23use crate::command::brigadier::{StringReader, SuggestionError, Suggestions};
24use crate::command::execution::{
25 CommandExecutionContext, CommandResultCallback, CommandSource, ExecutionCommandSource,
26 ExecutionStop,
27};
28use crate::command::sender::{CommandExecutionOwner, CommandSender};
29use crate::command::storage::DomainCommandStorage;
30use crate::command::{
31 COMMAND_REQUESTS_PER_TICK, COMMAND_RESUMPTIONS_PER_TICK, CommandCompletion, CommandDispatcher,
32 CommandQueueFull, CommandRegistry, CommandRequest, CommandRequestQueue,
33 PendingCommandExecutionQueue, client_permission_event, command_suggestions_packet,
34 command_tree_packet, create_registered_dispatcher,
35};
36#[cfg(any(test, feature = "test-framework"))]
37use crate::config::ResolvedDomainConfig;
38use crate::config::{ResolvedWorldConfig, RuntimeConfig, WorldsConfig, validate_login_security};
39use crate::entity::damage::DamageHistory;
40use crate::entity::{
41 Entity, EntityBase, PendingWorldChangeToken, RemovalReason, SharedEntity, change_entity_world,
42};
43
44use crate::chunk_saver::{ChunkStorage, PersistentEntity, registry::WorldStorageRegistry};
45use crate::level_data::{GameTimeSource, LevelDataManager, RespawnData, WorldGenerationSettings};
46use crate::permission::{
47 OP_GROUP, PermissionGroupManager, PermissionGroupManagerError, PermissionGroupUpdateError,
48 PermissionGroupsConfig, PermissionMetadataExpression, PermissionRuleExpression, PermissionSet,
49 PermissionSubjectIndex, PermissionSubjectState,
50};
51use crate::player::chunk_sender::{ChunkSender, EncodedChunk};
52use crate::player::connection::NetworkConnection;
53use crate::player::connection::ScheduledPlayPacket;
54use crate::player::player_data::{
55 PersistentEnderPearl, PersistentPlayerData, PersistentRootVehicle,
56};
57use crate::player::player_data_storage::{GlobalPlayerData, PlayerDataStorage};
58use crate::player::player_inventory::MenuRemovalStatus;
59use crate::player::{
60 DomainResidenceToken, GameProfile, KnownPlayer, KnownPlayerNameLookup, KnownPlayers, Player,
61 ProfileLookupError, ResetReason, is_valid_player_name, lookup_online_profile, offline_uuid,
62};
63use crate::portal::{
64 PortalKind, TeleportPostTransition, TeleportTransition, WorldChangeRequest, WorldSelection,
65 end_gateway, end_portal, nether_portal,
66};
67pub use crate::portal::{PreparedSpawn, WorldEntry};
68use crate::scoreboard::DomainScoreboards;
69use crate::server::jobs::{FnServerJob, ServerJobContext, ServerJobQueue};
70use crate::server::packet_processor::PacketProcessor;
71pub(crate) use crate::server::packet_processor::PlayerPacketTransition;
72use crate::server::registry_cache::RegistryCache;
73use crate::server::service_keys::ServiceKeyStore;
74use crate::server::worlds::WorldMap;
75use crate::world::player_spawn_finder::{PlayerSpawnSearch, PlayerSpawnSearchPoll};
76use crate::world::{PlayerMap, World, WorldConfig};
77use crate::worldgen::WorldGeneratorRegistry;
78use crate::worldgen::registry::GeneratorOutput;
79use crossbeam::queue::SegQueue;
80use glam::DVec3;
81use rayon::{ThreadPool, ThreadPoolBuilder};
82use rustc_hash::FxHashMap;
83use std::sync::atomic::AtomicI32;
84use std::{
85 collections::BTreeSet,
86 io, mem,
87 sync::{Arc, mpsc},
88 time::{Duration, Instant},
89};
90use steel_crypto::{key_store::KeyStore, signature::ProfileKeyValidator};
91use steel_protocol::packet_traits::{ClientPacket, EncodedPacket};
92use steel_protocol::packets::game::{
93 CCommandSuggestions, CEntityEvent, CLogin, CPlayerInfoUpdate, CRemovePlayerInfo,
94 CSetDefaultSpawnPosition, CSystemChat, CTabList, CTickingState, CTickingStep,
95 CommonPlayerSpawnInfo, PLAYER_INFO_INIT_ACTIONS, PlayerInfoEntry, RelativeMovement,
96};
97use steel_protocol::utils::ConnectionProtocol;
98use steel_registry::vanilla_game_rules::{
99 ALLOW_ENTERING_NETHER_USING_PORTALS, IMMEDIATE_RESPAWN, LIMITED_CRAFTING, REDUCED_DEBUG_INFO,
100};
101use steel_registry::{
102 RegistryEntry, dimension_type::DimensionTypeRef, vanilla_dimension_types, vanilla_entities,
103};
104use steel_utils::{
105 BlockPos, ChunkPos, Identifier,
106 locks::{AsyncMutex, SyncMutex, SyncRwLock},
107 text::DisplayResolutor,
108 threading::{DEBUG_STACK_SIZE, available_worker_threads},
109 translations,
110};
111use text_components::{Modifier, TextComponent, format::Color};
112use tick_rate_manager::{SprintReport, TickRateManager};
113use tokio::{
114 runtime::Runtime,
115 sync::Notify,
116 task::{JoinSet, spawn_blocking},
117 time::sleep,
118};
119use tokio_util::sync::CancellationToken;
120use uuid::Uuid;
121
122const TAB_LIST_UPDATE_INTERVAL: u64 = 20;
124const SEND_PLAYER_INFO_INTERVAL: u64 = 600;
126const COMMAND_DATA_AUTOSAVE_INTERVAL: Duration = Duration::from_secs(300);
129
130#[derive(Clone, Copy)]
131struct TabListTickStats {
132 tps: f32,
133 recent_mspt: f32,
134 average_mspt: f32,
135 p95_mspt: f32,
136}
137
138impl TabListTickStats {
139 fn capture(tick_manager: &TickRateManager) -> Self {
140 Self {
141 tps: tick_manager.get_tps(),
142 recent_mspt: tick_manager.get_smoothed_mspt(),
143 average_mspt: tick_manager.get_average_mspt(),
144 p95_mspt: tick_manager.get_p95(),
145 }
146 }
147}
148
149pub struct CommandDataSaveResults {
151 pub scoreboards: io::Result<usize>,
153 pub storage: io::Result<usize>,
155}
156
157mod known_players;
158
159use known_players::KnownPlayerCacheState;
160
161const CHUNK_SENDING_TPS: u64 = 20;
163
164const SLOW_CHUNK_TICK_THRESHOLD: Duration = Duration::from_millis(50);
166
167fn configured_chunk_generation_threads(configured_threads: Option<usize>) -> Option<usize> {
168 cap_positive_thread_count(configured_threads, available_worker_threads())
169}
170
171fn configured_chunk_encoding_threads(configured_threads: Option<usize>) -> Option<usize> {
172 cap_positive_thread_count(configured_threads, available_worker_threads())
173}
174
175fn cap_positive_thread_count(
176 configured_threads: Option<usize>,
177 available_threads: usize,
178) -> Option<usize> {
179 let configured_threads = configured_threads.filter(|&threads| threads > 0)?;
180 Some(configured_threads.min(available_threads.max(1)))
181}
182
183#[cfg(test)]
184mod tests;
185
186#[cfg(any(test, feature = "test-framework"))]
190pub async fn test_server_with_worlds_and_config(
191 default_domain: String,
192 domains: &[ResolvedDomainConfig],
193 loaded_worlds: &[Arc<World>],
194 player_permission_states: PermissionSubjectIndex,
195 config: Arc<RuntimeConfig>,
196) -> Result<Arc<Server>, String> {
197 let mut worlds = WorldMap::new(default_domain, domains, &[]);
198 for world in loaded_worlds {
199 worlds.insert(world.key.clone(), Arc::clone(world));
200 }
201 worlds.validate_game_times()?;
202 let scoreboards = DomainScoreboards::load(&worlds)
203 .await
204 .map_err(|error| format!("test scoreboards should load: {error}"))?;
205 let command_storage = DomainCommandStorage::load(&worlds)
206 .await
207 .map_err(|error| format!("test command storage should load: {error}"))?;
208 let player_data_storage = PlayerDataStorage::in_memory();
209 let registered_commands = create_registered_dispatcher(CommandRegistry::new())
210 .map_err(|error| format!("test commands should register: {error}"))?;
211 let command_permission_keys = registered_commands
212 .permissions
213 .iter()
214 .map(|permission| permission.as_str().to_owned())
215 .collect();
216 let permission_groups = PermissionGroupManager::transient(PermissionGroupsConfig::default())
217 .map_err(|error| format!("test permission groups should resolve: {error}"))?;
218 let registry_cache = RegistryCache::new(config.compression);
219 let chunk_encoding_pool = ThreadPoolBuilder::new()
220 .num_threads(1)
221 .build()
222 .map_err(|error| format!("test chunk encoding pool should initialize: {error}"))?;
223 let service_keys = ServiceKeyStore::new(None)
224 .map_err(|error| format!("test services key store should initialize: {error}"))?;
225 let damage_history = loaded_worlds
226 .first()
227 .and_then(|world| world.damage_history.upgrade())
228 .ok_or("test worlds must have a live damage history owner")?;
229 let shares_history = loaded_worlds.iter().all(|world| {
230 world
231 .damage_history
232 .upgrade()
233 .is_some_and(|history| Arc::ptr_eq(&history, &damage_history))
234 });
235 if !shares_history {
236 return Err("test worlds must share the server's history owner".to_owned());
237 }
238
239 Ok(Arc::new(Server {
240 damage_history,
241 config,
242 permission_groups,
243 cancel_token: CancellationToken::new(),
244 key_store: KeyStore::create(),
245 registry_cache,
246 worlds,
247 online_players: PlayerMap::new(),
248 player_admissions: SyncMutex::new(FxHashMap::default()),
249 player_admission_changed: Notify::new(),
250 server_tick_changed: Notify::new(),
251 tick_rate_manager: SyncRwLock::new(TickRateManager::new()),
252 scoreboards,
253 command_storage,
254 command_dispatcher: SyncRwLock::new(registered_commands.dispatcher),
255 command_permission_keys,
256 command_requests: CommandRequestQueue::new(),
257 packet_processor: PacketProcessor::new(),
258 chunk_encoding_pool: Arc::new(chunk_encoding_pool),
259 jobs: ServerJobQueue::new(),
260 player_data_storage,
261 player_permission_states: SyncRwLock::new(player_permission_states),
262 player_permission_updates: AsyncMutex::new(()),
263 known_players: SyncMutex::new(KnownPlayerCacheState::new(KnownPlayers::new())),
264 known_player_save_idle: Notify::new(),
265 profile_lookup_client: reqwest::Client::new(),
266 service_keys: Arc::new(service_keys),
267 pending_player_joins: PlayerJoinQueue::new(),
268 pending_player_disconnects: PlayerDisconnectQueue::new(),
269 pending_world_changes: SyncMutex::new(Vec::new()),
270 pending_domain_switches: SyncMutex::new(Vec::new()),
271 player_idle_timeout: AtomicI32::new(0),
272 }))
273}
274
275fn apply_default_spawn(player: &Arc<Player>, world: &Arc<World>, spawn: PreparedSpawn) {
276 player.base().set_position_local(spawn.position);
277 player.set_rotation(spawn.rotation);
278 player.restore_game_modes(world.default_gamemode(), None);
279 player
280 .abilities
281 .lock()
282 .update_for_game_mode(world.default_gamemode());
283}
284
285fn is_allowed_to_enter_portal(source_world: &World, target_world: &World) -> bool {
286 is_allowed_to_enter_portal_target(
287 is_nether_dimension_type(target_world),
288 source_world.get_game_rule(&ALLOW_ENTERING_NETHER_USING_PORTALS),
289 )
290}
291
292const fn is_allowed_to_enter_portal_target(
293 target_is_nether: bool,
294 allow_entering_nether_using_portals: bool,
295) -> bool {
296 if !target_is_nether {
297 return true;
298 }
299
300 allow_entering_nether_using_portals
301}
302
303fn can_teleport_between_worlds(
304 entity: &dyn Entity,
305 source_world: &World,
306 target_world: &World,
307 projectile_owner_seen_credits: impl Fn(&uuid::Uuid) -> Option<bool>,
308) -> bool {
309 if is_end_return_transition(source_world.dimension_type, target_world.dimension_type) {
310 return can_entity_return_from_end_to_overworld(entity, projectile_owner_seen_credits);
311 }
312
313 true
314}
315
316fn is_end_return_transition(
317 source_dimension_type: DimensionTypeRef,
318 target_dimension_type: DimensionTypeRef,
319) -> bool {
320 source_dimension_type == &vanilla_dimension_types::THE_END
321 && target_dimension_type == &vanilla_dimension_types::OVERWORLD
322}
323
324fn is_nether_dimension_type(world: &World) -> bool {
325 world.dimension_type == &vanilla_dimension_types::THE_NETHER
326}
327
328fn can_entity_return_from_end_to_overworld(
329 entity: &dyn Entity,
330 projectile_owner_seen_credits: impl Fn(&uuid::Uuid) -> Option<bool>,
331) -> bool {
332 if entity.entity_type() == &vanilla_entities::ENDER_PEARL
333 && entity
334 .projectile_owner_uuid()
335 .and_then(|uuid| projectile_owner_seen_credits(&uuid))
336 == Some(false)
337 {
338 return false;
339 }
340
341 direct_passengers_allow_end_return(entity)
342}
343
344fn direct_passengers_allow_end_return(entity: &dyn Entity) -> bool {
345 for passenger in entity.passengers() {
346 if passenger
347 .as_player()
348 .is_some_and(|player| !player.has_seen_credits())
349 {
350 return false;
351 }
352 }
353
354 true
355}
356
357fn local_respawn_data_for_world(world: &World) -> RespawnData {
358 let level_data = world.level_data.read();
359 let data = level_data.data();
360 RespawnData::of(world.key.clone(), data.spawn_pos(), data.spawn.angle, 0.0)
361}
362
363fn generation_settings_for_world(
364 world_entry: &ResolvedWorldConfig,
365 generator_output: &GeneratorOutput,
366) -> WorldGenerationSettings {
367 WorldGenerationSettings::from_generator_config(
368 world_entry.generator_config.generator().clone(),
369 &generator_output.config,
370 generator_output.dimension_type.key.clone(),
371 generator_output.dimension_type.min_y,
372 generator_output.dimension_type.height,
373 )
374}
375
376fn world_config_registries() -> Result<(WorldGeneratorRegistry, WorldStorageRegistry), String> {
377 let generator_registry = WorldGeneratorRegistry::new_with_builtins()
378 .map_err(|e| format!("failed to initialize world generator registry: {e}"))?;
379 let storage_registry = WorldStorageRegistry::new_with_builtins()
380 .map_err(|e| format!("failed to initialize world storage registry: {e}"))?;
381 Ok((generator_registry, storage_registry))
382}
383
384struct DomainPlayerState {
385 world: Arc<World>,
386 data: DomainPlayerData,
387 spawn_chunk_request: ChunkRequestHandle,
388}
389
390struct UnpreparedDomainPlayerState {
391 world: Arc<World>,
392 explicit_target: bool,
393 data: UnpreparedDomainPlayerData,
394}
395
396enum UnpreparedDomainPlayerData {
397 SavedRestored { data: Box<PersistentPlayerData> },
398 SavedWithoutLocation { data: Box<PersistentPlayerData> },
399 FirstVisit,
400}
401
402enum DomainPlayerData {
403 SavedRestored {
404 data: Box<PersistentPlayerData>,
405 },
406 SavedWithoutLocation {
407 data: Box<PersistentPlayerData>,
408 spawn: PreparedSpawn,
409 },
410 FirstVisit {
411 spawn: PreparedSpawn,
412 },
413}
414
415struct DomainSwitchRequest {
416 player: Arc<Player>,
417 target_domain: String,
418 target_world: Option<Arc<World>>,
419 entry: WorldEntry,
420 pending_token: PendingWorldChangeToken,
421}
422
423#[derive(Debug, thiserror::Error)]
425pub enum PlayerPermissionUpdateError<E> {
426 #[error("{0}")]
428 Edit(E),
429 #[error("unknown permission group '{0}'")]
431 UnknownGroup(String),
432 #[error("failed to update player permissions: {0}")]
434 Storage(io::Error),
435}
436
437impl<E> From<io::Error> for PlayerPermissionUpdateError<E> {
438 fn from(value: io::Error) -> Self {
439 Self::Storage(value)
440 }
441}
442
443mod permissions;
444
445#[cfg(test)]
446use permissions::validate_player_permission_group_update;
447
448mod player_admission;
449mod player_lifecycle;
450
451pub use player_admission::{DuplicatePlayerWaitError, PlayerJoinReservation};
452use player_admission::{PlayerAdmissionState, PlayerDisconnectQueue, PlayerJoinQueue};
453
454mod world_changes;
455
456use jobs::domain_switch::DomainSwitchJob;
457use jobs::teleport::{
458 EndGatewayTeleportJob, EndPortalTeleportJob, EnderPearlRestoreJob, NetherPortalTeleportJob,
459 RootVehicleRestoreJob, WorldSpawnTeleportJob, clear_pending_world_change,
460 portal_entity_still_valid,
461};
462
463pub struct Server {
465 pub(crate) damage_history: Arc<DamageHistory>,
466 pub config: Arc<RuntimeConfig>,
468 pub permission_groups: PermissionGroupManager,
470 pub cancel_token: CancellationToken,
472 pub key_store: KeyStore,
474 pub registry_cache: RegistryCache,
476 pub worlds: WorldMap,
478 online_players: PlayerMap,
480 player_admissions: SyncMutex<FxHashMap<Uuid, PlayerAdmissionState>>,
482 player_admission_changed: Notify,
484 server_tick_changed: Notify,
486 pub tick_rate_manager: SyncRwLock<TickRateManager>,
488 pub player_idle_timeout: AtomicI32,
491 pub scoreboards: DomainScoreboards,
493 pub(crate) command_storage: DomainCommandStorage,
495 command_dispatcher: SyncRwLock<CommandDispatcher>,
497 command_permission_keys: Vec<String>,
499 command_requests: CommandRequestQueue,
501 packet_processor: PacketProcessor,
503 chunk_encoding_pool: Arc<ThreadPool>,
505 pub jobs: ServerJobQueue,
507 pub player_data_storage: PlayerDataStorage,
509 player_permission_states: SyncRwLock<PermissionSubjectIndex>,
511 player_permission_updates: AsyncMutex<()>,
513 known_players: SyncMutex<KnownPlayerCacheState>,
515 known_player_save_idle: Notify,
517 profile_lookup_client: reqwest::Client,
519 service_keys: Arc<ServiceKeyStore>,
521 pending_player_joins: PlayerJoinQueue,
523 pending_player_disconnects: PlayerDisconnectQueue,
525 pub pending_world_changes: SyncMutex<Vec<(SharedEntity, WorldChangeRequest)>>,
527 pending_domain_switches: SyncMutex<Vec<DomainSwitchRequest>>,
529}
530
531struct GameTickTaskGuard {
532 server: Arc<Server>,
533 cancel_token: CancellationToken,
534}
535
536impl GameTickTaskGuard {
537 const fn new(server: Arc<Server>, cancel_token: CancellationToken) -> Self {
538 Self {
539 server,
540 cancel_token,
541 }
542 }
543}
544
545impl Drop for GameTickTaskGuard {
546 fn drop(&mut self) {
547 self.server.packet_processor.stop();
548 self.cancel_token.cancel();
549 }
550}
551
552impl Server {
553 pub fn current_tick(&self) -> u64 {
555 self.tick_rate_manager.read().tick_count
556 }
557
558 pub async fn wait_until_tick(&self, target_tick: u64) {
560 loop {
561 let tick_changed = self.server_tick_changed.notified();
562 tokio::pin!(tick_changed);
563 tick_changed.as_mut().enable();
564
565 if self.current_tick() >= target_tick {
566 return;
567 }
568
569 tick_changed.await;
570 }
571 }
572
573 pub(crate) fn permission_rule_suggestions(&self) -> Vec<String> {
574 let mut suggestions = self
575 .command_permission_keys
576 .iter()
577 .cloned()
578 .collect::<BTreeSet<_>>();
579 let config = self.permission_groups.config_snapshot();
580 for group in config.groups.values() {
581 suggestions.extend(group.allow.iter().cloned());
582 suggestions.extend(group.deny.iter().cloned());
583 }
584 for (_, state) in self.player_permission_states.read().entries() {
585 suggestions.extend(state.overrides().entries().iter().map(|entry| {
586 PermissionRuleExpression::new(entry.key().clone(), entry.context().clone())
587 .to_string()
588 }));
589 }
590 suggestions.into_iter().collect()
591 }
592
593 pub(crate) fn permission_metadata_suggestions(&self) -> Vec<String> {
594 let mut suggestions = BTreeSet::new();
595 let config = self.permission_groups.config_snapshot();
596 for group in config.groups.values() {
597 suggestions.extend(group.metadata.iter().map(|rule| rule.key.clone()));
598 }
599 for (_, state) in self.player_permission_states.read().entries() {
600 suggestions.extend(state.metadata_overrides().entries().iter().map(|entry| {
601 PermissionMetadataExpression::new(entry.key().clone(), entry.context().clone())
602 .to_string()
603 }));
604 }
605 suggestions.into_iter().collect()
606 }
607
608 pub async fn new(
610 chunk_runtime: Arc<Runtime>,
611 cancel_token: CancellationToken,
612 config: RuntimeConfig,
613 worlds_config: WorldsConfig,
614 permission_groups: PermissionGroupManager,
615 ) -> Result<Self, String> {
616 Self::new_with_commands(
617 chunk_runtime,
618 cancel_token,
619 config,
620 worlds_config,
621 permission_groups,
622 CommandRegistry::new(),
623 )
624 .await
625 }
626
627 #[expect(
629 clippy::too_many_lines,
630 reason = "server initialization is a single cohesive flow"
631 )]
632 pub async fn new_with_commands(
633 chunk_runtime: Arc<Runtime>,
634 cancel_token: CancellationToken,
635 config: RuntimeConfig,
636 worlds_config: WorldsConfig,
637 permission_groups: PermissionGroupManager,
638 command_registry: CommandRegistry,
639 ) -> Result<Self, String> {
640 validate_login_security(config.online_mode, config.encryption).map_err(str::to_owned)?;
641 let config = Arc::new(config);
642 init_globals();
643 log::info!(
644 "SteelMC is not affiliated with Mojang or Microsoft. Use is subject to the Minecraft EULA: https://aka.ms/MinecraftEULA"
645 );
646
647 let service_keys = Arc::new(
651 ServiceKeyStore::new(config.services_server.as_deref())
652 .map_err(|error| format!("failed to configure Minecraft services keys: {error}"))?,
653 );
654 let service_keys_ready = service_keys.start(cancel_token.clone());
655
656 let registry_cache = RegistryCache::new(config.compression);
657
658 let (generator_registry, storage_registry) = world_config_registries()?;
659 let resolved_worlds = worlds_config
660 .validate_and_resolve(&generator_registry, &storage_registry)
661 .map_err(|e| format!("failed to validate worlds.toml: {e}"))?;
662
663 let mut world_storage = storage_registry.resolve_worlds(&resolved_worlds)?;
664
665 let generation_pool: Arc<ThreadPool> = Arc::new({
666 let mut builder = ThreadPoolBuilder::new().thread_name(|i| format!("rayon-gen-{i}"));
667 if let Some(chunk_generation_threads) =
668 configured_chunk_generation_threads(config.chunk_generation_threads)
669 {
670 builder = builder.num_threads(chunk_generation_threads);
671 }
672 if cfg!(debug_assertions) {
674 builder = builder.stack_size(DEBUG_STACK_SIZE);
675 }
676 builder
677 .build()
678 .map_err(|e| format!("failed to create generation thread pool: {e}"))?
679 });
680 let chunk_encoding_pool = Arc::new({
681 let mut builder =
682 ThreadPoolBuilder::new().thread_name(|i| format!("rayon-chunk-encode-{i}"));
683 if let Some(chunk_encoding_threads) =
684 configured_chunk_encoding_threads(config.chunk_encoding_threads)
685 {
686 builder = builder.num_threads(chunk_encoding_threads);
687 }
688 builder
689 .build()
690 .map_err(|e| format!("failed to create chunk encoding thread pool: {e}"))?
691 });
692
693 let player_data_storage = PlayerDataStorage::from_selection(
694 resolved_worlds.save_path.clone(),
695 &resolved_worlds.player_storage,
696 )
697 .await
698 .map_err(|e| format!("failed to create player data storage: {e}"))?;
699 let player_permission_states = player_data_storage
700 .load_permission_subjects()
701 .await
702 .map_err(|error| format!("failed to load player permissions: {error}"))?;
703 let known_players = player_data_storage
704 .load_known_players()
705 .await
706 .map_err(|error| format!("failed to load known players: {error}"))?;
707 let mut worlds = WorldMap::new(
708 resolved_worlds.default_domain.clone(),
709 &resolved_worlds.domains,
710 &resolved_worlds.worlds,
711 );
712
713 let damage_history = Arc::new(DamageHistory::default());
714 let mut construct_world = async |world_entry: &ResolvedWorldConfig,
715 game_time_source: GameTimeSource|
716 -> Result<Arc<World>, String> {
717 let storage_output = world_storage
718 .remove(&world_entry.key)
719 .ok_or_else(|| format!("world {} has no resolved storage", world_entry.key))?;
720 let world_seed = LevelDataManager::load_seed_or_default(
721 storage_output.level_data_path.as_deref(),
722 world_entry.seed,
723 )
724 .await
725 .map_err(|e| {
726 format!(
727 "failed to load level data seed for {}: {e}",
728 world_entry.key
729 )
730 })?;
731 let generator_output = generator_registry
732 .create(
733 storage_output.level_data_path.as_deref(),
734 &world_entry.generator_config,
735 world_seed,
736 Arc::clone(&generation_pool),
737 )
738 .map_err(|e| format!("failed to create generator for {}: {e}", world_entry.key))?;
739 let generation_settings = generation_settings_for_world(world_entry, &generator_output);
740 let world = World::new_with_config_and_encoding_pool(
741 Arc::clone(&chunk_runtime),
742 world_entry.key.clone(),
743 generator_output.dimension_type,
744 world_seed,
745 WorldConfig {
746 damage_history: Arc::clone(&damage_history),
747 game_time_source,
748 storage: storage_output.storage,
749 level_data_path: storage_output
750 .level_data_path
751 .map(|path| path.to_string_lossy().into_owned()),
752 generator: Arc::new(generator_output.generator),
753 generation_settings,
754 view_distance: config.view_distance,
755 simulation_distance: config.simulation_distance,
756 max_chained_neighbor_updates: config.max_chained_neighbor_updates,
757 compression: config.compression,
758 is_flat: generator_output.is_flat,
759 sea_level: generator_output.sea_level,
760 default_gamemode: world_entry.default_gamemode,
761 difficulty: world_entry.difficulty,
762 },
763 Arc::clone(&generation_pool),
764 Arc::clone(&chunk_encoding_pool),
765 )
766 .await
767 .map_err(|e| format!("failed to create world {}: {e}", world_entry.key))?;
768 world
769 .initialize_spawn_if_needed()
770 .await
771 .map_err(|e| format!("failed to initialize spawn for {}: {e}", world_entry.key))?;
772 Ok(world)
773 };
774 for domain in &resolved_worlds.domains {
775 let primary_config = resolved_worlds
776 .worlds
777 .iter()
778 .find(|world| world.key == domain.default_world && world.domain == domain.name)
779 .ok_or_else(|| {
780 format!(
781 "domain {} has no configured primary {}",
782 domain.name, domain.default_world
783 )
784 })?;
785 let primary = construct_world(primary_config, GameTimeSource::Primary).await?;
786 let clock = Arc::clone(&primary.game_time);
787 worlds.insert(primary_config.key.clone(), primary);
788 for world_entry in resolved_worlds
789 .worlds
790 .iter()
791 .filter(|world| world.domain == domain.name && world.key != domain.default_world)
792 {
793 let world =
794 construct_world(world_entry, GameTimeSource::Derived(Arc::clone(&clock)))
795 .await?;
796 worlds.insert(world_entry.key.clone(), world);
797 }
798 }
799 worlds.validate_game_times()?;
800
801 let scoreboards = DomainScoreboards::load(&worlds)
802 .await
803 .map_err(|error| format!("failed to load domain scoreboards: {error}"))?;
804 let command_storage = DomainCommandStorage::load(&worlds)
805 .await
806 .map_err(|error| format!("failed to load domain command storage: {error}"))?;
807 let registered_commands = create_registered_dispatcher(command_registry)
808 .map_err(|error| format!("failed to register commands: {error}"))?;
809 let command_permission_keys = registered_commands
810 .permissions
811 .into_iter()
812 .map(|permission| permission.as_str().to_owned())
813 .collect();
814
815 if config.online_mode && service_keys_ready.await.is_err() {
818 log::error!("Minecraft services key fetch task stopped before its initial attempt");
819 }
820
821 Ok(Server {
822 damage_history,
823 config,
824 permission_groups,
825 cancel_token,
826 key_store: KeyStore::create(),
827 worlds,
828 online_players: PlayerMap::new(),
829 player_admissions: SyncMutex::new(FxHashMap::default()),
830 player_admission_changed: Notify::new(),
831 server_tick_changed: Notify::new(),
832 registry_cache,
833 tick_rate_manager: SyncRwLock::new(TickRateManager::new()),
834 player_idle_timeout: AtomicI32::new(0),
835 scoreboards,
836 command_storage,
837 command_dispatcher: SyncRwLock::new(registered_commands.dispatcher),
838 command_permission_keys,
839 command_requests: CommandRequestQueue::new(),
840 packet_processor: PacketProcessor::new(),
841 chunk_encoding_pool,
842 jobs: ServerJobQueue::new(),
843 player_data_storage,
844 player_permission_states: SyncRwLock::new(player_permission_states),
845 player_permission_updates: AsyncMutex::new(()),
846 known_players: SyncMutex::new(KnownPlayerCacheState::new(known_players)),
847 known_player_save_idle: Notify::new(),
848 profile_lookup_client: reqwest::Client::new(),
849 service_keys,
850 pending_player_joins: PlayerJoinQueue::new(),
851 pending_player_disconnects: PlayerDisconnectQueue::new(),
852 pending_world_changes: SyncMutex::new(vec![]),
853 pending_domain_switches: SyncMutex::new(vec![]),
854 })
855 }
856
857 pub fn profile_key_signature_validator(&self) -> Option<Arc<ProfileKeyValidator>> {
859 self.service_keys.profile_key_validator()
860 }
861
862 #[must_use]
864 pub fn enforces_secure_chat(&self) -> bool {
865 self.config.enforce_secure_chat
866 && self.config.online_mode
867 && self.profile_key_signature_validator().is_some()
868 }
869
870 pub async fn save_command_storage(&self) -> io::Result<usize> {
872 self.command_storage.save(&self.worlds).await
873 }
874
875 pub async fn save_command_data(&self) -> CommandDataSaveResults {
877 CommandDataSaveResults {
878 scoreboards: self.scoreboards.save(&self.worlds).await,
879 storage: self.save_command_storage().await,
880 }
881 }
882
883 pub fn submit_command(
885 &self,
886 sender: CommandSender,
887 command: String,
888 ) -> Result<(), CommandQueueFull> {
889 self.command_requests.submit(CommandRequest::Execute {
890 owner: CommandExecutionOwner::capture(sender, self),
891 command,
892 })
893 }
894
895 pub(crate) fn submit_command_suggestions(
896 &self,
897 player: Arc<Player>,
898 transaction_id: i32,
899 input: String,
900 ) -> Result<(), CommandQueueFull> {
901 self.command_requests.submit(CommandRequest::Suggestions {
902 owner: CommandExecutionOwner::capture(CommandSender::Player(player), self),
903 transaction_id,
904 input,
905 })
906 }
907
908 pub(crate) fn schedule_play_packet(
910 &self,
911 player: Arc<Player>,
912 packet: ScheduledPlayPacket,
913 payload_bytes: usize,
914 ) {
915 self.packet_processor
916 .schedule(player, packet, payload_bytes);
917 }
918
919 pub(crate) fn begin_player_packet_transition(
921 &self,
922 player: &Arc<Player>,
923 ) -> Option<PlayerPacketTransition> {
924 if player.connection.closed() || !player.session.is_current_player(player) {
925 return None;
926 }
927 self.packet_processor.pause_player_session(&player.session)
928 }
929
930 pub(crate) fn finish_player_packet_transition(
932 &self,
933 transition: PlayerPacketTransition,
934 ) -> bool {
935 self.packet_processor.resume_player_session(transition)
936 }
937
938 pub(crate) fn discard_player_packets(&self, player: &Player) {
940 self.packet_processor
941 .discard_player_session(&player.session);
942 }
943
944 pub fn command_completions(
946 self: &Arc<Self>,
947 sender: CommandSender,
948 input: &str,
949 ) -> Vec<CommandCompletion> {
950 if !CommandExecutionOwner::capture(sender.clone(), self).is_current(self) {
951 return Vec::new();
952 }
953 match self.build_command_suggestions(sender, input) {
954 Ok(suggestions) => {
955 let range = suggestions.range();
956 suggestions
957 .list()
958 .iter()
959 .map(|suggestion| {
960 CommandCompletion::new(
961 range.start(),
962 range.len(),
963 suggestion.text().to_owned(),
964 )
965 })
966 .collect()
967 }
968 Err(error) => {
969 tracing::warn!(%error, "failed to build command suggestions");
970 Vec::new()
971 }
972 }
973 }
974}