Skip to main content

chunkedge_server_common/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod despawn;
4mod uuid;
5
6use std::num::NonZeroU32;
7use std::time::{Duration, Instant};
8
9use bevy_app::PluginsState;
10use bevy_app::prelude::*;
11use bevy_ecs::prelude::*;
12use chunkedge_protocol::CompressionThreshold;
13pub use despawn::*;
14
15pub use crate::uuid::*;
16
17/// Minecraft's standard ticks per second (TPS).
18pub const DEFAULT_TPS: NonZeroU32 = match NonZeroU32::new(20) {
19    Some(n) => n,
20    None => unreachable!(),
21};
22
23#[derive(Clone, Resource)]
24pub struct ServerSettings {
25    /// The target ticks per second (TPS) of the server. This is the number of
26    /// game updates that should occur in one second.
27    ///
28    /// On each game update (tick), the server is expected to update game logic
29    /// and respond to packets from clients. Once this is complete, the server
30    /// will sleep for any remaining time until a full tick duration has passed.
31    ///
32    /// Note that the official Minecraft client only processes packets at 20hz,
33    /// so there is little benefit to a tick rate higher than the default 20.
34    ///
35    /// # Default Value
36    ///
37    /// [`DEFAULT_TPS`]
38    pub tick_rate: NonZeroU32,
39    /// The compression threshold to use for compressing packets. For a
40    /// compression threshold of `Some(N)`, packets with encoded lengths >= `N`
41    /// are compressed while all others are not. `None` disables compression
42    /// completely.
43    ///
44    /// If the server is used behind a proxy on the same machine, you will
45    /// likely want to disable compression.
46    ///
47    /// # Default Value
48    ///
49    /// Compression is enabled with an unspecified value. This value may
50    /// change in future versions.
51    pub compression_threshold: CompressionThreshold,
52}
53
54impl Default for ServerSettings {
55    fn default() -> Self {
56        Self {
57            tick_rate: DEFAULT_TPS,
58            compression_threshold: CompressionThreshold(256),
59        }
60    }
61}
62
63pub struct ServerPlugin;
64
65impl Plugin for ServerPlugin {
66    fn build(&self, app: &mut App) {
67        let settings = app
68            .world_mut()
69            .get_resource_or_insert_with(ServerSettings::default)
70            .clone();
71
72        app.insert_resource(Server {
73            current_tick: 0,
74            threshold: settings.compression_threshold,
75            tick_rate: settings.tick_rate,
76        });
77
78        let tick_period = Duration::from_secs_f64(f64::from(settings.tick_rate.get()).recip());
79
80        // Make the app loop forever at the configured TPS.
81        app.set_runner(tick_loop_runner(tick_period));
82
83        fn increment_tick_counter(mut server: ResMut<Server>) {
84            server.current_tick += 1;
85        }
86
87        app.add_systems(Last, (increment_tick_counter, despawn_marked_entities));
88    }
89}
90
91/// Maximum lag we try to make up before giving up and resetting the clock.
92/// Matches vanilla's "Can't keep up" threshold of 2 seconds.
93const MAX_CATCH_UP: Duration = Duration::from_secs(2);
94
95/// Builds the server's tick loop runner.
96///
97/// Behaves like vanilla Minecraft's scheduler: it targets an absolute per-tick
98/// deadline so that `thread::sleep` overshoot is reclaimed on the following tick, keeping the long-run average
99/// at exactly the configured TPS. When a tick runs long it catches up by
100/// skipping the sleep; if it falls more than [`MAX_CATCH_UP`] behind it drops
101/// the backlog instead of spiraling.
102///
103/// This replaces Bevy's [`ScheduleRunnerPlugin`](bevy_app::ScheduleRunnerPlugin),
104/// which resets its clock every iteration and so never reclaims sleep overshoot.
105fn tick_loop_runner(tick_period: Duration) -> impl FnOnce(App) -> AppExit {
106    move |mut app: App| {
107        // Drive plugins to readiness
108        if app.plugins_state() != PluginsState::Cleaned {
109            while app.plugins_state() == PluginsState::Adding {
110                bevy_tasks::tick_global_task_pools_on_main_thread();
111            }
112            app.finish();
113            app.cleanup();
114        }
115
116        let mut next_tick = Instant::now();
117        loop {
118            app.update();
119            if let Some(exit) = app.should_exit() {
120                return exit;
121            }
122
123            next_tick += tick_period;
124            let now = Instant::now();
125            if now < next_tick {
126                // Ahead of schedule: wait until the next tick is due.
127                std::thread::sleep(next_tick - now);
128            } else if now - next_tick > MAX_CATCH_UP {
129                // Too far behind: abandon the backlog so we don't death-spiral.
130                next_tick = now;
131            }
132            // loop immediately to catch up.
133        }
134    }
135}
136
137/// Contains global server state accessible as a [`Resource`].
138#[derive(Resource, Clone)]
139pub struct Server {
140    /// Incremented on every tick.
141    current_tick: i64,
142    threshold: CompressionThreshold,
143    tick_rate: NonZeroU32,
144}
145
146impl Server {
147    /// Returns the number of ticks that have elapsed since the server began.
148    pub fn current_tick(&self) -> i64 {
149        self.current_tick
150    }
151
152    /// Returns the server's [compression
153    /// threshold](ServerSettings::compression_threshold).
154    pub fn compression_threshold(&self) -> CompressionThreshold {
155        self.threshold
156    }
157
158    // Returns the server's [tick rate](ServerPlugin::tick_rate).
159    pub fn tick_rate(&self) -> NonZeroU32 {
160        self.tick_rate
161    }
162}