Skip to main content

steel_core/server/
mod.rs

1//! This module contains the `Server` struct, which is the main entry point for the server.
2mod broadcasting;
3/// Tick-polled server jobs.
4pub mod jobs;
5mod packet_processor;
6mod pregen;
7/// The registry cache for the server.
8pub mod registry_cache;
9mod run_loop;
10mod service_keys;
11mod tick_overload;
12/// The tick rate manager for the server.
13pub mod tick_rate_manager;
14mod world_tick_workers;
15/// Domain-aware loaded world map.
16pub 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
122/// Interval in ticks between tab list updates (20 ticks = 1 second).
123const TAB_LIST_UPDATE_INTERVAL: u64 = 20;
124/// Interval in ticks between player info broadcasts (600 ticks = 30 seconds).
125const SEND_PLAYER_INFO_INTERVAL: u64 = 600;
126/// Wall-clock interval between saves of command-owned persistent server data.
127/// Matches vanilla's intended five-minute autosave cadence.
128const 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
149/// Results from saving every command-owned persistent data set.
150pub struct CommandDataSaveResults {
151    /// Number of dirty domain scoreboards written, or the save error.
152    pub scoreboards: io::Result<usize>,
153    /// Number of dirty domain command-storage values written, or the save error.
154    pub storage: io::Result<usize>,
155}
156
157mod known_players;
158
159use known_players::KnownPlayerCacheState;
160
161/// Tick rate for the chunk sending loop.
162const CHUNK_SENDING_TPS: u64 = 20;
163
164/// Work duration at which background chunk work is considered slow.
165const 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/// Builds a server around already loaded worlds without starting any server loops.
187///
188/// Used by Steel tests and by the Flint test adapter.
189#[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/// Failure while atomically editing one player's persisted permission state.
424#[derive(Debug, thiserror::Error)]
425pub enum PlayerPermissionUpdateError<E> {
426    /// The caller rejected the proposed edit.
427    #[error("{0}")]
428    Edit(E),
429    /// The edit assigns a group that is not configured.
430    #[error("unknown permission group '{0}'")]
431    UnknownGroup(String),
432    /// The permission snapshot could not be persisted.
433    #[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
463/// The main server struct.
464pub struct Server {
465    pub(crate) damage_history: Arc<DamageHistory>,
466    /// Runtime configuration (view distance, compression, etc.).
467    pub config: Arc<RuntimeConfig>,
468    /// Runtime permission groups and their persistence boundary.
469    pub permission_groups: PermissionGroupManager,
470    /// The cancellation token for graceful shutdown.
471    pub cancel_token: CancellationToken,
472    /// The key store for the server.
473    pub key_store: KeyStore,
474    /// The registry cache for the server.
475    pub registry_cache: RegistryCache,
476    /// A list of all the worlds on the server.
477    pub worlds: WorldMap,
478    /// Players currently connected to the server, independent of world membership.
479    online_players: PlayerMap,
480    /// UUIDs reserved by a join or disconnect/save lifecycle transition.
481    player_admissions: SyncMutex<FxHashMap<Uuid, PlayerAdmissionState>>,
482    /// Wakes verified logins waiting for an older session with the same UUID to leave.
483    player_admission_changed: Notify,
484    /// Wakes connection lifecycle work waiting for a specific server tick.
485    server_tick_changed: Notify,
486    /// The tick rate manager for the server.
487    pub tick_rate_manager: SyncRwLock<TickRateManager>,
488    /// The number of minutes required for a player to be idle for them to be kicked (timed out) from the server.
489    /// If this is equal to 0, no kicking will happen.
490    pub player_idle_timeout: AtomicI32,
491    /// Command scoreboards isolated by Steel domain.
492    pub scoreboards: DomainScoreboards,
493    /// Command NBT storage isolated by Steel domain.
494    pub(crate) command_storage: DomainCommandStorage,
495    /// Saves and dispatches commands to appropriate handlers.
496    command_dispatcher: SyncRwLock<CommandDispatcher>,
497    /// Steel-owned permission keys exposed for command autocomplete.
498    command_permission_keys: Vec<String>,
499    /// Command work submitted from connection and console tasks.
500    command_requests: CommandRequestQueue,
501    /// Decoded serverbound play packets handled during the inter-tick phase.
502    packet_processor: PacketProcessor,
503    /// Dedicated worker pool for CPU-heavy chunk persistence and packet encoding.
504    chunk_encoding_pool: Arc<ThreadPool>,
505    /// Jobs resumed from a known point in the server game tick.
506    pub jobs: ServerJobQueue,
507    /// Player data storage for saving/loading player state.
508    pub player_data_storage: PlayerDataStorage,
509    /// Persisted permission state indexed by player UUID.
510    player_permission_states: SyncRwLock<PermissionSubjectIndex>,
511    /// Serializes persistence and cache publication for player permission edits.
512    player_permission_updates: AsyncMutex<()>,
513    /// Player identities and coalesced persistence state.
514    known_players: SyncMutex<KnownPlayerCacheState>,
515    /// Wakes shutdown when the single known-player save worker becomes idle.
516    known_player_save_idle: Notify,
517    /// HTTP client used by online-mode name-to-profile lookups.
518    profile_lookup_client: reqwest::Client,
519    /// Cached Mojang service keys used to validate player-key certificates.
520    service_keys: Arc<ServiceKeyStore>,
521    /// Player joins prepared by async I/O and finalized at the game tick safe point.
522    pending_player_joins: PlayerJoinQueue,
523    /// Disconnected players waiting to be detached at the next game tick safe point.
524    pending_player_disconnects: PlayerDisconnectQueue,
525    /// Queued world changes to process after the tick.
526    pub pending_world_changes: SyncMutex<Vec<(SharedEntity, WorldChangeRequest)>>,
527    /// Queued domain switches to process after world ticks.
528    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    /// Returns the current server tick number.
554    pub fn current_tick(&self) -> u64 {
555        self.tick_rate_manager.read().tick_count
556    }
557
558    /// Waits until the server reaches `target_tick`.
559    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    /// Creates a new server with only Steel's built-in commands.
609    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    /// Creates a new server and atomically merges startup command extensions after built-ins.
628    #[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        // Authlib starts this fetch alongside server initialization and waits on first use.
648        // It runs whatever the login mode, because `handle_chat_session_update` reads these
649        // keys with no online-mode gate, as vanilla does.
650        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            // Debug builds have deep call chains in density functions that overflow the default 2 MB stack
673            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        // Steel finishes the initial attempt before opening its listener, except offline,
816        // where `enforces_secure_chat` needs online mode so nothing acts on the result.
817        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    /// Returns the current player-certificate validator, if service keys are available.
858    pub fn profile_key_signature_validator(&self) -> Option<Arc<ProfileKeyValidator>> {
859        self.service_keys.profile_key_validator()
860    }
861
862    /// Returns whether secure chat can currently be enforced.
863    #[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    /// Saves all dirty domain command storage through domain default worlds.
871    pub async fn save_command_storage(&self) -> io::Result<usize> {
872        self.command_storage.save(&self.worlds).await
873    }
874
875    /// Saves all command-owned persistent data while allowing each data set to fail independently.
876    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    /// Queues a command for execution at the start of the next game tick.
884    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    /// Schedules a decoded play packet for the inter-tick packet phase.
909    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    /// Pauses later packets while `player` is replaced by a new incarnation.
920    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    /// Resumes packets retained by an exact player-replacement transition.
931    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    /// Discards all pending packet work for a closed player session.
939    pub(crate) fn discard_player_packets(&self, player: &Player) {
940        self.packet_processor
941            .discard_player_session(&player.session);
942    }
943
944    /// Returns Brigadier completions visible to a command sender.
945    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}