1use super::tick_overload::TickOverloadGuard;
2use super::world_tick_workers::{WorldTickWorkerError, WorldTickWorkers};
3use super::{
4 Arc, CCommandSuggestions, CHUNK_SENDING_TPS, COMMAND_DATA_AUTOSAVE_INTERVAL,
5 COMMAND_REQUESTS_PER_TICK, COMMAND_RESUMPTIONS_PER_TICK, CancellationToken, ChunkPos,
6 ChunkSender, CommandExecutionContext, CommandExecutionOwner, CommandRequest,
7 CommandResultCallback, CommandSender, CommandSource, Duration, EncodedChunk,
8 ExecutionCommandSource, ExecutionStop, GameTickTaskGuard, GlobalPlayerData, Instant, JoinSet,
9 MenuRemovalStatus, NetworkConnection, PendingCommandExecutionQueue, PersistentPlayerData,
10 Player, SEND_PLAYER_INFO_INTERVAL, SLOW_CHUNK_TICK_THRESHOLD, Server, StringReader,
11 SuggestionError, Suggestions, TAB_LIST_UPDATE_INTERVAL, TabListTickStats, ThreadPool, World,
12 command_suggestions_packet, sleep, spawn_blocking,
13};
14
15use steel_registry::vanilla_custom_stats;
16use steel_utils::threading::{available_worker_threads, worker_threads_for_available};
17use steel_utils::translations;
18
19impl Server {
20 pub(super) fn advance_server_tick(&self) -> (u64, bool) {
21 let (tick_count, runs_normally) = {
22 let mut tick_manager = self.tick_rate_manager.write();
23 tick_manager.tick();
24 let runs_normally = tick_manager.runs_normally();
25 tick_manager.increment_tick_count();
26 (tick_manager.tick_count, runs_normally)
27 };
28 self.server_tick_changed.notify_waiters();
29 (tick_count, runs_normally)
30 }
31
32 pub async fn run(self: Arc<Self>, cancel_token: CancellationToken) {
34 self.packet_processor.open_after_tick();
35 let packet_worker_count =
36 worker_threads_for_available(self.config.packet_workers, available_worker_threads());
37 let mut packet_handles = Vec::with_capacity(packet_worker_count);
38 for worker_id in 0..packet_worker_count {
39 let s = Arc::clone(&self);
40 let t = cancel_token.clone();
41 packet_handles.push(tokio::spawn(async move {
42 if let Err(error) = spawn_blocking(move || s.packet_processor.run(&s)).await {
43 log::error!("Gameplay packet worker {worker_id} failed: {error}");
44 t.cancel();
45 }
46 }));
47 }
48 let packet_supervisor_cancel = cancel_token.clone();
49 let packet_workers = async move {
50 for handle in packet_handles {
51 if let Err(error) = handle.await {
52 log::error!("Gameplay packet supervisor failed: {error}");
53 packet_supervisor_cancel.cancel();
54 }
55 }
56 };
57 let game_handle = {
58 let s = Arc::clone(&self);
59 let t = cancel_token.clone();
60 let task_guard = GameTickTaskGuard::new(Arc::clone(&self), cancel_token.clone());
61 tokio::spawn(async move {
62 let _task_guard = task_guard;
63 s.run_game_tick(t).await;
64 })
65 };
66 let chunk_send_handle = {
67 let s = Arc::clone(&self);
68 let t = cancel_token.clone();
69 tokio::spawn(async move { s.run_chunk_sending_tick(t).await })
70 };
71 let ((), game_result, chunk_send_result) =
72 tokio::join!(packet_workers, game_handle, chunk_send_handle);
73 for (task, result) in [
74 ("Game tick", game_result),
75 ("Chunk sending tick", chunk_send_result),
76 ] {
77 if let Err(error) = result {
78 log::error!("{task} task failed: {error}");
79 }
80 }
81 }
82
83 pub async fn save_and_shutdown(&self) {
91 if let Err(error) = self.flush_known_players().await {
92 log::error!("Failed to flush known player cache during shutdown: {error}");
93 }
94
95 let players = self.get_players();
96 for player in &players {
97 let _ = self.reserve_player_disconnect(player);
100 player.disconnect(translations::MULTIPLAYER_DISCONNECT_SERVER_SHUTDOWN.msg());
101 assert_eq!(
102 player.remove_all_menus(),
103 MenuRemovalStatus::Complete,
104 "shutdown menu removal must run after packet processing stops"
105 );
106 }
107
108 for world in self.worlds.values() {
109 world.chunk_map.stop_generation_refill_loop();
110 world.chunk_map.task_tracker.close();
111 world.chunk_map.task_tracker.wait().await;
112 }
113
114 let mut players_to_save = Vec::new();
115 for player in players {
116 let domain = player.get_world().domain().to_owned();
117 player.award_custom_stat(&vanilla_custom_stats::LEAVE_GAME);
118 let data = PersistentPlayerData::from_player(&player);
119 player.store_ender_pearls_with_player();
120 players_to_save.push((player, domain, data));
121 }
122
123 self.damage_history.clear();
124 log::info!("Saving world data...");
125 let command_data = self.save_command_data().await;
126 match command_data.scoreboards {
127 Ok(saved) => log::info!("Saved {saved} domain scoreboards"),
128 Err(error) => log::error!("Failed to save domain scoreboards: {error}"),
129 }
130 match command_data.storage {
131 Ok(saved) => log::info!("Saved {saved} domain command storages"),
132 Err(error) => log::error!("Failed to save domain command storage: {error}"),
133 }
134 let mut total_saved = 0;
135 for world in self.worlds.values() {
136 world.cleanup(&mut total_saved).await;
137 }
138 log::info!("Saved {total_saved} chunks");
139
140 log::info!("Saving player data...");
141 let mut saved = 0;
142 for (player, domain, data) in players_to_save {
143 let uuid = player.gameprofile.id();
144 match self
145 .player_data_storage
146 .save_domain_data(&domain, uuid, &data)
147 .await
148 {
149 Ok(()) => {
150 saved += 1;
151 }
152 Err(e) => {
153 log::error!("Failed to save player {uuid} domain data during shutdown: {e}");
154 }
155 }
156 if let Err(e) = self
157 .player_data_storage
158 .save_global(
159 uuid,
160 &GlobalPlayerData {
161 last_active_domain: domain,
162 },
163 )
164 .await
165 {
166 log::error!("Failed to save player {uuid} global data during shutdown: {e}");
167 }
168 }
169 log::info!("Saved {saved} players");
170 }
171
172 #[expect(
174 clippy::too_many_lines,
175 reason = "the ordered tick phases and their shutdown joins remain easier to audit together"
176 )]
177 async fn run_game_tick(self: Arc<Self>, cancel_token: CancellationToken) {
178 let world_tick_workers = match WorldTickWorkers::spawn(self.worlds.values()) {
179 Ok(workers) => workers,
180 Err(error) => {
181 log::error!("Failed to start world tick workers: {error}");
182 cancel_token.cancel();
183 return;
184 }
185 };
186 let mut next_tick_time = Instant::now();
187 let mut overload_guard = TickOverloadGuard::new();
188 let mut next_command_data_autosave = Instant::now() + COMMAND_DATA_AUTOSAVE_INTERVAL;
189 let mut player_info_ticks = 0_u64;
190 let mut pending_command_executions = PendingCommandExecutionQueue::<CommandSource>::new();
191 let mut player_disconnect_saves = JoinSet::new();
192 let mut command_data_autosaves = JoinSet::new();
193
194 loop {
195 if cancel_token.is_cancelled() {
196 break;
197 }
198
199 let (nanoseconds_per_tick, should_sprint_this_tick) = {
200 let mut tick_manager = self.tick_rate_manager.write();
201 let nanoseconds_per_tick = tick_manager.nanoseconds_per_tick;
202 let (should_sprint, sprint_report) = tick_manager.check_should_sprint_this_tick();
203 drop(tick_manager);
204
205 if let Some(report) = sprint_report {
206 self.broadcast_sprint_report(&report);
207 self.broadcast_ticking_state();
208 }
209
210 (nanoseconds_per_tick, should_sprint)
211 };
212
213 if should_sprint_this_tick {
214 next_tick_time = Instant::now();
215 overload_guard.restart_report_gap(next_tick_time);
216 } else {
217 let now = Instant::now();
218 if now < next_tick_time {
219 tokio::select! {
220 () = cancel_token.cancelled() => break,
221 () = sleep(next_tick_time - now) => {}
222 }
223 } else {
224 overload_guard.skip_backlog_if_overloaded(
225 now,
226 &mut next_tick_time,
227 nanoseconds_per_tick,
228 );
229 }
230 next_tick_time += Duration::from_nanos(nanoseconds_per_tick);
231 }
232
233 if cancel_token.is_cancelled() {
234 break;
235 }
236
237 let tick_start = Instant::now();
238 self.packet_processor.close_for_tick().await;
239 self.start_player_disconnect_saves(&mut player_disconnect_saves);
240
241 let (tick_count, runs_normally) = self.advance_server_tick();
242
243 self.tick_pending_command_executions(&mut pending_command_executions);
244 self.tick_command_requests(&mut pending_command_executions);
245 if let Err(error) = self
246 .tick_worlds_game(&world_tick_workers, tick_count, runs_normally)
247 .await
248 {
249 log::error!("World game tick failed: {error}");
250 cancel_token.cancel();
251 break;
252 }
253 player_info_ticks += 1;
254 if player_info_ticks > SEND_PLAYER_INFO_INTERVAL {
255 let _span = tracing::trace_span!("broadcast_latency").entered();
256 self.broadcast_player_latency_updates();
257 player_info_ticks = 0;
258 }
259 self.tick_jobs(tick_count, runs_normally);
260 self.process_player_joins();
261
262 {
263 let server = Arc::clone(&self);
264 let _ =
265 spawn_blocking(move || server.process_world_changes(tick_count, runs_normally))
266 .await;
267 }
268
269 self.process_domain_switches();
270
271 self.tick_command_data_autosave(
272 &mut next_command_data_autosave,
273 &mut command_data_autosaves,
274 );
275
276 let tab_list_tick_stats = self.record_tick_and_capture_tab_stats(
277 tick_count,
278 tick_start.elapsed().as_nanos() as u64,
279 );
280
281 if let Some(tick_stats) = tab_list_tick_stats {
282 self.broadcast_tab_list(tick_stats);
283 }
284
285 if should_sprint_this_tick {
286 let mut tick_manager = self.tick_rate_manager.write();
287 tick_manager.end_tick_work();
288 }
289
290 self.packet_processor.open_after_tick();
291 if should_sprint_this_tick || Instant::now() >= next_tick_time {
292 self.packet_processor.wait_for_overload_progress().await;
293 }
294 }
295
296 self.jobs.cancel_all();
297 pending_command_executions.cancel_all();
298 self.command_requests.clear();
299 self.packet_processor.stop();
300 self.start_player_disconnect_saves(&mut player_disconnect_saves);
301 self.pending_player_disconnects.clear();
302 while let Some(result) = player_disconnect_saves.join_next().await {
303 if let Err(error) = result {
304 log::error!("Player disconnect save task failed during shutdown: {error}");
305 }
306 }
307 while let Some(result) = command_data_autosaves.join_next().await {
308 if let Err(error) = result {
309 log::error!("Command data autosave task failed during shutdown: {error}");
310 }
311 }
312 }
313
314 fn record_tick_and_capture_tab_stats(
315 &self,
316 tick_count: u64,
317 tick_duration_nanos: u64,
318 ) -> Option<TabListTickStats> {
319 let mut tick_manager = self.tick_rate_manager.write();
320 tick_manager.record_tick_time(tick_duration_nanos);
321 tick_count
322 .is_multiple_of(TAB_LIST_UPDATE_INTERVAL)
323 .then(|| TabListTickStats::capture(&tick_manager))
324 }
325
326 async fn autosave_command_data(&self) {
327 tracing::debug!("Command data autosave started");
328 let results = self.save_command_data().await;
329 match results.scoreboards {
330 Ok(saved) => tracing::debug!(saved, "Domain scoreboard autosave completed"),
331 Err(error) => tracing::error!(%error, "Domain scoreboard autosave failed"),
332 }
333 match results.storage {
334 Ok(saved) => tracing::debug!(saved, "Domain command-storage autosave completed"),
335 Err(error) => tracing::error!(%error, "Domain command-storage autosave failed"),
336 }
337 }
338
339 fn tick_command_data_autosave(
340 self: &Arc<Self>,
341 next_autosave: &mut Instant,
342 saves: &mut JoinSet<()>,
343 ) {
344 while let Some(result) = saves.try_join_next() {
345 if let Err(error) = result {
346 log::error!("Command data autosave task failed: {error}");
347 }
348 }
349 if Instant::now() < *next_autosave {
350 return;
351 }
352 if saves.is_empty() {
353 let server = Arc::clone(self);
354 saves.spawn(async move {
355 server.autosave_command_data().await;
356 });
357 } else {
358 tracing::warn!("Skipping command data autosave while the previous save runs");
359 }
360 *next_autosave = Instant::now() + COMMAND_DATA_AUTOSAVE_INTERVAL;
361 }
362
363 fn tick_pending_command_executions(
364 &self,
365 pending: &mut PendingCommandExecutionQueue<CommandSource>,
366 ) {
367 let stats = pending.tick(COMMAND_RESUMPTIONS_PER_TICK, |owner| owner.is_current(self));
368 if stats.polled == COMMAND_RESUMPTIONS_PER_TICK && stats.pending > 0 {
369 tracing::debug!(
370 polled = stats.polled,
371 finished = stats.finished,
372 pending = stats.pending,
373 "Command resumption tick reached per-tick processing limit"
374 );
375 }
376 }
377
378 fn tick_command_requests(
379 self: &Arc<Self>,
380 pending: &mut PendingCommandExecutionQueue<CommandSource>,
381 ) {
382 let mut handled = 0;
383 for _ in 0..COMMAND_REQUESTS_PER_TICK {
384 let Some(request) = self
385 .command_requests
386 .pop_front_runnable(|owner| !pending.blocks(owner.key()))
387 else {
388 break;
389 };
390 handled += 1;
391
392 match request {
393 CommandRequest::Execute { owner, command } => {
394 if !owner.is_current(self) {
395 continue;
396 }
397 self.execute_command_request(pending, owner, &command);
398 }
399 CommandRequest::Suggestions {
400 owner,
401 transaction_id,
402 input,
403 } => {
404 if !owner.is_current(self) {
405 continue;
406 }
407 let Some(player) = owner.sender().get_player() else {
408 tracing::error!("command suggestion request has a non-player owner");
409 continue;
410 };
411 self.send_command_suggestions(player, transaction_id, &input);
412 }
413 }
414 }
415
416 if handled == COMMAND_REQUESTS_PER_TICK {
417 tracing::debug!(handled, "Command request tick reached its processing limit");
418 }
419 }
420
421 fn execute_command_request(
422 self: &Arc<Self>,
423 pending: &mut PendingCommandExecutionQueue<CommandSource>,
424 owner: CommandExecutionOwner,
425 command: &str,
426 ) {
427 let source = CommandSource::new(owner.sender().clone(), Arc::clone(self));
428 let command = command.strip_prefix('/').unwrap_or(command);
429 let chain = {
430 let dispatcher = self.command_dispatcher.read();
431 let parse = dispatcher.parse(command, source.clone());
432 dispatcher.context_chain(parse)
433 };
434 let chain = match chain {
435 Ok(chain) => chain,
436 Err(error) => {
437 source.handle_error(&error, false);
438 return;
439 }
440 };
441
442 let mut execution = CommandExecutionContext::for_source(&source);
443 execution.queue_initial_command(chain, source, CommandResultCallback::empty());
444 if execution.run() == ExecutionStop::Suspended && !pending.push_suspended(owner, execution)
445 {
446 tracing::error!("suspended command execution could not be retained");
447 }
448 }
449
450 fn send_command_suggestions(
451 self: &Arc<Self>,
452 player: &Arc<Player>,
453 transaction_id: i32,
454 input: &str,
455 ) {
456 let suggestions =
457 self.build_command_suggestions(CommandSender::Player(Arc::clone(player)), input);
458 match suggestions {
459 Ok(suggestions) => {
460 player.send_packet(command_suggestions_packet(transaction_id, &suggestions));
461 }
462 Err(error) => {
463 tracing::warn!(%error, "failed to build command suggestions");
464 player.send_packet(CCommandSuggestions::new(transaction_id, 0, 0, Vec::new()));
465 }
466 }
467 }
468
469 pub(super) fn build_command_suggestions(
470 self: &Arc<Self>,
471 sender: CommandSender,
472 input: &str,
473 ) -> Result<Suggestions, SuggestionError> {
474 let source = CommandSource::new(sender, Arc::clone(self));
475 let mut reader = StringReader::new(input);
476 if reader.peek() == Some('/') {
477 reader.skip();
478 }
479 let dispatcher = self.command_dispatcher.read();
480 let parse = dispatcher.parse_reader(reader, source);
481 dispatcher.completion_suggestions(&parse)
482 }
483
484 async fn run_chunk_sending_tick(self: Arc<Self>, cancel_token: CancellationToken) {
486 let nanos_per_tick = 1_000_000_000 / CHUNK_SENDING_TPS;
487 let mut next_tick_time = Instant::now();
488
489 loop {
490 if cancel_token.is_cancelled() {
491 break;
492 }
493
494 let now = Instant::now();
495 if now < next_tick_time {
496 tokio::select! {
497 () = cancel_token.cancelled() => break,
498 () = sleep(next_tick_time - now) => {}
499 }
500 }
501 next_tick_time += Duration::from_nanos(nanos_per_tick);
502
503 if cancel_token.is_cancelled() {
504 break;
505 }
506
507 let server = Arc::clone(&self);
508 let _ = spawn_blocking(move || {
509 server.tick_chunk_sending();
510 })
511 .await;
512 }
513 }
514
515 fn tick_chunk_sending(&self) {
520 let tick_start = Instant::now();
521 for world in self.worlds.values() {
522 let mut encode_cache = rustc_hash::FxHashMap::default();
523 world.players.iter_players(|_uuid, player| {
524 Self::send_chunks_for_player(
525 player,
526 world,
527 &mut encode_cache,
528 self.chunk_encoding_pool.as_ref(),
529 );
530 true
531 });
532 }
533
534 let elapsed = tick_start.elapsed();
535 if elapsed >= SLOW_CHUNK_TICK_THRESHOLD {
536 tracing::warn!(?elapsed, "Chunk sending tick slow");
537 }
538 }
539
540 fn send_chunks_for_player(
543 player: &Arc<Player>,
544 world: &Arc<World>,
545 encode_cache: &mut rustc_hash::FxHashMap<ChunkPos, EncodedChunk>,
546 encoding_pool: &ThreadPool,
547 ) {
548 let chunk_pos = *player.last_chunk_pos.lock();
549 let connection = &player.connection;
550
551 let prepared = {
553 let mut sender = player.chunk_sender().lock();
554 sender.prepare_batch(world, chunk_pos, &player.chunk_send_epoch)
555 };
556
557 let Some(batch) = prepared else {
558 return;
559 };
560
561 let compression = connection.compression();
563 let encoded = ChunkSender::encode_batch(&batch, encode_cache, compression, encoding_pool);
564
565 let tracking_view = player.last_tracking_view.lock();
569 let Some(view) = *tracking_view else {
570 return;
571 };
572 if !world.contains_player(player) {
573 return;
574 }
575 let sent_chunks = {
576 let mut sender = player.chunk_sender().lock();
577 sender.commit_batch(&batch, encoded, connection, &player.chunk_send_epoch)
578 };
579
580 if sent_chunks.is_empty() {
581 return;
582 }
583
584 let sent_chunks = player.chunk_sender().lock().sent_chunks_snapshot();
585 world
586 .entity_tracker()
587 .update_player(player, &view, |chunk| sent_chunks.contains(&chunk));
588 }
589
590 #[tracing::instrument(level = "trace", skip(self, workers), name = "tick_worlds")]
591 pub(super) async fn tick_worlds_game(
592 &self,
593 workers: &WorldTickWorkers,
594 tick_count: u64,
595 runs_normally: bool,
596 ) -> Result<(), WorldTickWorkerError> {
597 if runs_normally {
598 self.worlds.advance_domain_game_times();
599 }
600 self.damage_history.expire();
601 let all_timings = workers.tick_all(tick_count, runs_normally).await?;
602 for (i, timings) in all_timings.iter().enumerate() {
603 if timings.elapsed < SLOW_CHUNK_TICK_THRESHOLD {
604 continue;
605 }
606 let cm = &timings.chunk_map;
607 let scheduling = &cm.scheduling;
608 tracing::warn!(
609 world = i,
610 elapsed = ?timings.elapsed,
611 tick_count,
612 entity_tick = ?timings.entity_tick,
613 ticket_updates = ?scheduling.ticket_updates,
614 block_entity_unloads = ?scheduling.block_entity_unloads,
615 readiness_demotions = ?scheduling.readiness_demotions,
616 lifecycle_commit = ?scheduling.lifecycle_commit,
617 readiness_reconcile = ?scheduling.readiness_reconcile,
618 post_process_generation = ?scheduling.post_process_generation,
619 post_process_chunk_count = scheduling.post_process_chunk_count,
620 post_process_position_count = scheduling.post_process_position_count,
621 readiness_candidate_count = scheduling.readiness_candidate_count,
622 ticking_snapshot_rebuild = ?scheduling.ticking_snapshot_rebuild,
623 rebuilt_ticking_chunk_count = scheduling.rebuilt_ticking_chunk_count,
624 schedule_generation = ?scheduling.schedule_generation,
625 scheduled_count = scheduling.scheduled_count,
626 run_generation = ?scheduling.run_generation,
627 process_unloads = ?scheduling.process_unloads,
628 broadcast_changes = ?cm.broadcast_changes,
629 collect_tickable = ?cm.collect_tickable,
630 tick_chunks = ?cm.tick_chunks,
631 tick_block_entities = ?cm.tick_block_entities,
632 tickable_count = cm.tickable_count,
633 total_chunks = cm.total_chunks,
634 lookup_cache_holder_hits = cm.lookup_cache.holder_hits,
635 lookup_cache_missing_hits = cm.lookup_cache.missing_hits,
636 lookup_cache_scc_lookups = cm.lookup_cache.scc_lookups,
637 lookup_cache_foreign_map_bypasses = cm.lookup_cache.foreign_map_bypasses,
638 lookup_cache_evictions = cm.lookup_cache.evictions,
639 "Game tick slow"
640 );
641 }
642 Ok(())
643 }
644
645 pub(super) fn tick_jobs(self: &Arc<Self>, tick_count: u64, runs_normally: bool) {
646 let stats = self
647 .jobs
648 .tick(Arc::downgrade(self), tick_count, runs_normally);
649 if stats.polled > 0 && stats.pending > 0 && tick_count.is_multiple_of(100) {
650 tracing::debug!(
651 polled = stats.polled,
652 finished = stats.finished,
653 pending = stats.pending,
654 "Server jobs pending"
655 );
656 }
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use std::sync::Arc;
663
664 use super::Server;
665
666 use crate::{
667 player::ResetReason,
668 test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk},
669 };
670 use rustc_hash::FxHashMap;
671 use steel_utils::ChunkPos;
672
673 #[test]
674 fn chunk_send_commit_rechecks_live_world_membership() {
675 let world_fixture = fresh_test_world("chunk_send_membership_revalidation");
676 let world = &world_fixture.world;
677 let center = ChunkPos::new(0, 0);
678 insert_ready_full_chunk(world, center);
679 let player = TestPlayerBuilder::new(Arc::clone(world), "ChunkTester", 1).build();
680 assert!(world.add_player(Arc::clone(&player), ResetReason::InitialJoin));
681 assert!(world.players.remove_player_sync(&player).is_some());
682
683 let encoding_pool = rayon::ThreadPoolBuilder::new().num_threads(1).build();
684 let Ok(encoding_pool) = encoding_pool else {
685 panic!("test chunk encoding pool should initialize");
686 };
687 let mut encode_cache = FxHashMap::default();
688 Server::send_chunks_for_player(&player, world, &mut encode_cache, &encoding_pool);
689
690 let sender = player.chunk_sender().lock();
691 assert!(sender.pending_chunks.contains(¢er));
692 assert!(!sender.is_chunk_sent(center));
693 assert_eq!(sender.unacknowledged_batch_count_for_test(), 0);
694 drop(sender);
695
696 assert!(world.players.insert(Arc::clone(&player)));
697 world.remove_player_for_world_change(&player);
698 }
699}