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;
10/// The tick rate manager for the server.
11pub mod tick_rate_manager;
12mod world_tick_workers;
13/// Domain-aware loaded world map.
14pub 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
115/// Interval in ticks between tab list updates (20 ticks = 1 second).
116const TAB_LIST_UPDATE_INTERVAL: u64 = 20;
117/// Interval in ticks between player info broadcasts (600 ticks = 30 seconds).
118/// Matches vanilla `PlayerList.SEND_PLAYER_INFO_INTERVAL`.
119const SEND_PLAYER_INFO_INTERVAL: u64 = 600;
120/// Wall-clock interval between saves of command-owned persistent server data.
121/// Matches vanilla's intended five-minute autosave cadence.
122const 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
143/// Results from saving every command-owned persistent data set.
144pub struct CommandDataSaveResults {
145    /// Number of dirty domain scoreboards written, or the save error.
146    pub scoreboards: io::Result<usize>,
147    /// Number of dirty domain command-storage values written, or the save error.
148    pub storage: io::Result<usize>,
149}
150
151mod known_players;
152
153use known_players::KnownPlayerCacheState;
154
155/// Tick rate for the chunk sending loop.
156const CHUNK_SENDING_TPS: u64 = 20;
157
158/// Work duration at which background chunk work is considered slow.
159const 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/// Failure while atomically editing one player's persisted permission state.
358#[derive(Debug, thiserror::Error)]
359pub enum PlayerPermissionUpdateError<E> {
360    /// The caller rejected the proposed edit.
361    #[error("{0}")]
362    Edit(E),
363    /// The edit assigns a group that is not configured.
364    #[error("unknown permission group '{0}'")]
365    UnknownGroup(String),
366    /// The permission snapshot could not be persisted.
367    #[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
396/// The main server struct.
397pub struct Server {
398    /// Runtime configuration (view distance, compression, etc.).
399    pub config: Arc<RuntimeConfig>,
400    /// Runtime permission groups and their persistence boundary.
401    pub permission_groups: PermissionGroupManager,
402    /// The cancellation token for graceful shutdown.
403    pub cancel_token: CancellationToken,
404    /// The key store for the server.
405    pub key_store: KeyStore,
406    /// The registry cache for the server.
407    pub registry_cache: RegistryCache,
408    /// A list of all the worlds on the server.
409    pub worlds: WorldMap,
410    /// Players currently connected to the server, independent of world membership.
411    online_players: PlayerMap,
412    /// UUIDs reserved by a join or disconnect/save lifecycle transition.
413    player_admissions: SyncMutex<FxHashMap<Uuid, PlayerAdmissionState>>,
414    /// The tick rate manager for the server.
415    pub tick_rate_manager: SyncRwLock<TickRateManager>,
416    /// Command scoreboards isolated by Steel domain.
417    pub scoreboards: DomainScoreboards,
418    /// Command NBT storage isolated by Steel domain.
419    pub(crate) command_storage: DomainCommandStorage,
420    /// Saves and dispatches commands to appropriate handlers.
421    command_dispatcher: SyncRwLock<CommandDispatcher>,
422    /// Steel-owned permission keys exposed for command autocomplete.
423    command_permission_keys: Vec<String>,
424    /// Command work submitted from connection and console tasks.
425    command_requests: CommandRequestQueue,
426    /// Decoded serverbound play packets handled during the inter-tick phase.
427    packet_processor: PacketProcessor,
428    /// Dedicated worker pool for CPU-heavy chunk persistence and packet encoding.
429    chunk_encoding_pool: Arc<ThreadPool>,
430    /// Jobs resumed from a known point in the server game tick.
431    pub jobs: ServerJobQueue,
432    /// Player data storage for saving/loading player state.
433    pub player_data_storage: PlayerDataStorage,
434    /// Persisted permission state indexed by player UUID.
435    player_permission_states: SyncRwLock<PermissionSubjectIndex>,
436    /// Serializes persistence and cache publication for player permission edits.
437    player_permission_updates: AsyncMutex<()>,
438    /// Player identities and coalesced persistence state.
439    known_players: SyncMutex<KnownPlayerCacheState>,
440    /// Wakes shutdown when the single known-player save worker becomes idle.
441    known_player_save_idle: Notify,
442    /// HTTP client used by online-mode name-to-profile lookups.
443    profile_lookup_client: reqwest::Client,
444    /// Player joins prepared by async I/O and finalized at the game tick safe point.
445    pending_player_joins: PlayerJoinQueue,
446    /// Disconnected players waiting to be detached at the next game tick safe point.
447    pending_player_disconnects: PlayerDisconnectQueue,
448    /// Queued world changes to process after the tick.
449    pub pending_world_changes: SyncMutex<Vec<(SharedEntity, WorldChangeRequest)>>,
450    /// Queued domain switches to process after world ticks.
451    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    /// Creates a new server with only Steel's built-in commands.
512    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    /// Creates a new server and atomically merges startup command extensions after built-ins.
531    #[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            // Debug builds have deep call chains in density functions that overflow the default 2 MB stack
565            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    /// Saves all dirty domain command storage through domain default worlds.
717    pub async fn save_command_storage(&self) -> io::Result<usize> {
718        self.command_storage.save(&self.worlds).await
719    }
720
721    /// Saves all command-owned persistent data while allowing each data set to fail independently.
722    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    /// Queues a command for execution at the start of the next game tick.
730    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    /// Schedules a decoded play packet for the inter-tick packet phase.
755    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    /// Returns Brigadier completions visible to a command sender.
766    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}