Pumpkin/crates/pumpkin/src/server/tick_rate_manager.rs
2026-08-19 11:53:53 +02:00

224 lines
8.0 KiB
Rust

use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering};
use std::time::Instant;
use crossbeam::atomic::AtomicCell;
use pumpkin_data::translation;
use pumpkin_protocol::java::client::play::{CSystemChatMessage, CTickingState, CTickingStep};
use pumpkin_util::text::{TextComponent, color::NamedColor};
use crate::entity::player::Player;
use crate::server::Server;
const NANOSECONDS_PER_SECOND: i64 = 1_000_000_000;
pub struct ServerTickRateManager {
tickrate: AtomicCell<f32>,
nanoseconds_per_tick: AtomicI64,
frozen_ticks_to_run: AtomicI32,
run_game_elements: AtomicBool,
is_frozen: AtomicBool,
// Sprinting state
remaining_sprint_ticks: AtomicI64,
sprint_tick_start_time: AtomicCell<Instant>,
sprint_time_spend: AtomicI64,
scheduled_current_sprint_ticks: AtomicI64,
previous_is_frozen: AtomicBool,
}
impl ServerTickRateManager {
#[must_use]
pub fn new(tickrate: f32) -> Self {
Self {
tickrate: AtomicCell::new(tickrate),
nanoseconds_per_tick: AtomicI64::new(NANOSECONDS_PER_SECOND / tickrate as i64),
frozen_ticks_to_run: AtomicI32::new(0),
run_game_elements: AtomicBool::new(true),
is_frozen: AtomicBool::new(false),
remaining_sprint_ticks: AtomicI64::new(0),
sprint_tick_start_time: AtomicCell::new(Instant::now()),
sprint_time_spend: AtomicI64::new(0),
scheduled_current_sprint_ticks: AtomicI64::new(0),
previous_is_frozen: AtomicBool::new(false),
}
}
}
impl ServerTickRateManager {
pub fn tick(&self) {
let frozen_ticks = self.frozen_ticks_to_run.load(Ordering::Relaxed);
let run_game = !self.is_frozen.load(Ordering::Relaxed) || frozen_ticks > 0;
self.run_game_elements.store(run_game, Ordering::Relaxed);
if frozen_ticks > 0 {
self.frozen_ticks_to_run.fetch_sub(1, Ordering::Relaxed);
}
}
// Getters
pub fn tickrate(&self) -> f32 {
self.tickrate.load()
}
pub fn nanoseconds_per_tick(&self) -> i64 {
self.nanoseconds_per_tick.load(Ordering::Relaxed)
}
pub fn is_frozen(&self) -> bool {
self.is_frozen.load(Ordering::Relaxed)
}
pub fn runs_normally(&self) -> bool {
self.run_game_elements.load(Ordering::Relaxed)
}
pub fn is_sprinting(&self) -> bool {
self.remaining_sprint_ticks.load(Ordering::Relaxed) > 0
}
pub fn is_stepping_forward(&self) -> bool {
self.frozen_ticks_to_run.load(Ordering::Relaxed) > 0
}
pub fn set_tick_rate(&self, server: &Server, rate: f32) {
self.tickrate.store(rate.max(1.0));
self.nanoseconds_per_tick.store(
(NANOSECONDS_PER_SECOND as f64 / f64::from(self.tickrate.load())) as i64,
Ordering::Relaxed,
);
// server.on_tick_rate_changed(); // Might need this hook if autosave interval depends on it
self.update_state_to_clients(server);
}
pub fn set_frozen(&self, server: &Server, frozen: bool) {
self.is_frozen.store(frozen, Ordering::Relaxed);
self.update_state_to_clients(server);
}
pub fn step_game_if_paused(&self, server: &Server, ticks: i32) -> bool {
if !self.is_frozen() {
return false;
}
self.frozen_ticks_to_run.store(ticks, Ordering::Relaxed);
self.update_step_ticks(server);
true
}
pub fn stop_stepping(&self, server: &Server) -> bool {
if self.is_stepping_forward() {
self.frozen_ticks_to_run.store(0, Ordering::Relaxed);
self.update_step_ticks(server);
true
} else {
false
}
}
pub fn request_game_to_sprint(&self, server: &Server, ticks: i64) -> bool {
let was_sprinting = self.is_sprinting();
self.sprint_time_spend.store(0, Ordering::Relaxed);
self.scheduled_current_sprint_ticks
.store(ticks, Ordering::Relaxed);
self.remaining_sprint_ticks.store(ticks, Ordering::Relaxed);
self.previous_is_frozen
.store(self.is_frozen(), Ordering::Relaxed);
self.set_frozen(server, false);
was_sprinting
}
pub fn stop_sprinting(&self, server: &Server) -> bool {
if self.is_sprinting() {
self.finish_tick_sprint(server);
true
} else {
false
}
}
/// Records the start time of a sprint tick's workload.
pub fn start_sprint_tick_work(&self) {
self.sprint_tick_start_time.store(Instant::now());
}
/// Records the time spent on the sprint tick's work and decrements the remaining sprint ticks.
/// Returns `true` if the sprint has just finished.
pub fn end_sprint_tick_work(&self) -> bool {
let spent = Instant::now().duration_since(self.sprint_tick_start_time.load());
self.sprint_time_spend
.fetch_add(spent.as_nanos() as i64, Ordering::Relaxed);
// fetch_sub returns the *previous* value. If it was 1, it is now 0, and the sprint is over.
self.remaining_sprint_ticks.fetch_sub(1, Ordering::Relaxed) == 1
}
pub fn finish_tick_sprint(&self, server: &Server) {
let total_sprinted_ticks = self.scheduled_current_sprint_ticks.load(Ordering::Relaxed)
- self.remaining_sprint_ticks.load(Ordering::Relaxed);
let time_spent_nanos = self.sprint_time_spend.load(Ordering::Relaxed);
let inner_message = if total_sprinted_ticks > 0 && time_spent_nanos > 0 {
let time_spent_ms = time_spent_nanos as f64 / 1_000_000.0;
let tps = (total_sprinted_ticks as f64 * 1000.0) / time_spent_ms;
let mspt = time_spent_ms / total_sprinted_ticks as f64;
TextComponent::translate_cross(
translation::java::COMMANDS_TICK_SPRINT_REPORT,
translation::java::COMMANDS_TICK_SPRINT_REPORT,
[
TextComponent::text(format!("{tps:.2}")),
TextComponent::text(format!("{mspt:.2}")),
],
)
} else {
// This is the message for `/tick sprint stop` or a zero-tick sprint.
TextComponent::translate_cross(
translation::java::COMMANDS_TICK_SPRINT_STOP_SUCCESS,
translation::java::COMMANDS_TICK_SPRINT_STOP_SUCCESS,
[],
)
};
// Construct the final component with the [Server: ...] wrapper
let final_report = TextComponent::text("[Server: ")
.add_child(inner_message)
.add_child(TextComponent::text("]"))
.italic()
.color_named(NamedColor::Gray);
// Send as a system chat message, which does not add a sender prefix.
server.broadcast_packet_all(&CSystemChatMessage::new(&final_report, false));
// Reset state after sending the report
self.scheduled_current_sprint_ticks
.store(0, Ordering::Relaxed);
self.sprint_time_spend.store(0, Ordering::Relaxed);
self.remaining_sprint_ticks.store(0, Ordering::Relaxed);
self.set_frozen(server, self.previous_is_frozen.load(Ordering::Relaxed));
// server.on_tick_rate_changed();
}
fn update_state_to_clients(&self, server: &Server) {
server.broadcast_packet_all(&CTickingState::new(
self.tickrate.load(),
self.is_frozen.load(Ordering::Relaxed),
));
}
fn update_step_ticks(&self, server: &Server) {
server.broadcast_packet_all(&CTickingStep::new(
self.frozen_ticks_to_run.load(Ordering::Relaxed).into(),
));
}
pub async fn update_joining_player(&self, player: &Player) {
if player.client.java_version() >= pumpkin_util::version::JavaMinecraftVersion::V_1_20_3 {
player
.send_client_packet(&CTickingState::new(self.tickrate(), self.is_frozen()))
.await;
player
.send_client_packet(&CTickingStep::new(
self.frozen_ticks_to_run.load(Ordering::Relaxed).into(),
))
.await;
}
}
}