224 lines
8.0 KiB
Rust
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;
|
|
}
|
|
}
|
|
}
|