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}