Debug charts: F3+2 frame and tick times, F3+3 ping and bandwidth

DebugScreenOverlay's FpsDebugChart, TpsDebugChart, PingDebugChart and
BandwidthDebugChart with vanilla's scales, colours and places: 240 samples
(LocalSampleLogger), F3+2 and F3+3 toggles that show the overlay and exclude
each other, the overlay's "Debug charts" lines, and the play-state ping
request and pong response packets the ping chart uses on servers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-15 10:16:02 +02:00
parent 3ed8abd25e
commit 6ae018055d
6 changed files with 649 additions and 5 deletions
+536
View File
@@ -0,0 +1,536 @@
//! The debug charts (26.3 `debugchart`): F3+2 shows the frame times and, in
//! single player, the integrated server's tick times; F3+3 the ping and, on a
//! remote server, the bandwidth. Each chart keeps the last 240 samples
//! (`LocalSampleLogger`) and draws one bar per sample over a 60 pixel high
//! box (`AbstractDebugChart`).
use crate::{
font::Font,
hud_layout::{GuiDraw, argb, fill},
text::Text,
};
use std::{
collections::VecDeque,
time::{Duration, Instant},
};
/// `LocalSampleLogger.CAPACITY`.
pub const CAPACITY: usize = 240;
/// `AbstractDebugChart.CHART_HEIGHT`.
pub const CHART_HEIGHT: i32 = 60;
/// `TpsDebugDimensions`: a tick sample's values.
pub const FULL_TICK: usize = 0;
pub const TICK_SERVER_METHOD: usize = 1;
pub const SCHEDULED_TASKS: usize = 2;
pub const IDLE: usize = 3;
/// `LocalSampleLogger`: the last [`CAPACITY`] samples, oldest first; a
/// sample has up to four values (`TpsDebugDimensions`), the first the main.
#[derive(Clone, Debug, Default)]
pub struct SampleLogger {
samples: VecDeque<[i64; 4]>,
}
impl SampleLogger {
/// `logSample`.
pub fn log(&mut self, value: i64) {
self.log_full([value, 0, 0, 0]);
}
/// `logFullSample`.
pub fn log_full(&mut self, sample: [i64; 4]) {
if self.samples.len() == CAPACITY {
self.samples.pop_front();
}
self.samples.push_back(sample);
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn reset(&mut self) {
self.samples.clear();
}
fn get(&self, index: usize, dimension: usize) -> i64 {
self.samples[index][dimension]
}
}
/// `DebugScreenOverlay`'s chart switches and the samples behind them.
#[derive(Clone, Debug, Default)]
pub struct DebugCharts {
/// `renderFpsCharts` (F3+2).
pub fps: bool,
/// `renderNetworkCharts` (F3+3).
pub network: bool,
/// `frameTimeLogger`: nanoseconds between frames.
pub frame_times: SampleLogger,
/// `tickTimeLogger`: the integrated server's ticks.
pub tick_times: SampleLogger,
/// `pingLogger`: milliseconds.
pub ping: SampleLogger,
/// `bandwidthLogger`: bytes received per tick.
pub bandwidth: SampleLogger,
previous_tick: Option<Instant>,
bytes_seen: Option<i64>,
}
impl DebugCharts {
/// `toggleFpsCharts`; `overlay` is the debug overlay's visibility.
pub fn toggle_fps(&mut self, overlay: &mut bool) {
self.fps = !*overlay || !self.fps;
if self.fps {
*overlay = true;
self.network = false;
}
}
/// `toggleNetworkCharts`.
pub fn toggle_network(&mut self, overlay: &mut bool) {
self.network = !*overlay || !self.network;
if self.network {
*overlay = true;
self.fps = false;
}
}
/// `reset`: leaving a world.
pub fn reset(&mut self) {
self.tick_times.reset();
self.ping.reset();
self.bandwidth.reset();
self.previous_tick = None;
self.bytes_seen = None;
}
/// One integrated server tick that started at `started` and ran its
/// tick method for `method` (`MinecraftServer.logFullTickTime` and
/// `logTickMethodTime`): the full tick runs from one start to the next,
/// and what the tick method leaves of it is idle.
pub fn log_tick(&mut self, started: Instant, method: Duration) {
let method = method.as_nanos() as i64;
let full = self
.previous_tick
.map_or(method, |p| (started - p).as_nanos() as i64)
.max(method);
self.previous_tick = Some(started);
self.tick_times.log_full([full, method, 0, full - method]);
}
/// `BandwidthDebugMonitor.tick`: the bytes received since the last
/// tick, from the connection's running total.
pub fn log_bandwidth(&mut self, total: i64) {
if let Some(seen) = self.bytes_seen {
self.bandwidth.log(total - seen);
}
self.bytes_seen = Some(total);
}
/// The charts along the bottom of the screen (`extractRenderState`):
/// frame times left and tick times right, or bandwidth left (remote
/// servers only) and ping right. `mspt` is the level's milliseconds
/// per tick.
pub fn draw(
&self,
font: &Font,
width: i32,
height: i32,
framerate_limit: u32,
mspt: f32,
remote: bool,
) -> Vec<GuiDraw> {
let mut out = Vec::new();
let max_width = width / 2;
let chart_width = Chart::width(max_width);
if self.fps {
let fps = Chart::Fps { framerate_limit };
out.extend(fps.draw(font, &self.frame_times, 0, chart_width, height));
if !self.tick_times.is_empty() {
let tps = Chart::Tps { mspt };
out.extend(tps.draw(
font,
&self.tick_times,
width - chart_width,
chart_width,
height,
));
}
}
if self.network {
if remote {
out.extend(Chart::Bandwidth.draw(font, &self.bandwidth, 0, chart_width, height));
}
out.extend(Chart::Ping.draw(
font,
&self.ping,
width - chart_width,
chart_width,
height,
));
}
out
}
}
/// The four charts and what each needs to scale its bars.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Chart {
/// `FpsDebugChart`: frame durations in nanoseconds; the frame rate limit
/// option (`maxFps`).
Fps { framerate_limit: u32 },
/// `TpsDebugChart`: tick samples in nanoseconds; the level's
/// milliseconds per tick.
Tps { mspt: f32 },
/// `PingDebugChart`: round trips in milliseconds.
Ping,
/// `BandwidthDebugChart`: bytes received per tick.
Bandwidth,
}
/// The labels' colour and the backdrop of the box and shaded labels.
const LABEL: u32 = 0xFFE0_E0E0;
const BACKDROP: u32 = 0x9050_5050;
const WHITE: u32 = 0xFFFF_FFFF;
const GREEN: u32 = 0xFF00_FF00;
const YELLOW: u32 = 0xFFFF_FF00;
const RED: u32 = 0xFFFF_0000;
const CYAN: u32 = 0xFF00_FFFF;
/// `BandwidthDebugChart.MID_COLOR`.
const LAVENDER: u32 = 0xFFA0_A0FF;
/// `TpsDebugChart`'s bar parts.
const TICK_METHOD_COLOR: u32 = 0xFF99_1111;
const TASK_COLOR: u32 = 0xFFBA_995F;
const OTHER_COLOR: u32 = 0xFF5F_0E8C;
const MEGABYTE: f64 = 1_048_576.0;
impl Chart {
/// `AbstractDebugChart.getWidth`.
pub fn width(max_width: i32) -> i32 {
(CAPACITY as i32 + 2).min(max_width)
}
/// `getFullHeight`: the box and the min/avg/max line above it.
pub fn full_height() -> i32 {
CHART_HEIGHT + 9
}
/// `AbstractDebugChart.extractRenderState`.
pub fn draw(
self,
font: &Font,
samples: &SampleLogger,
left: i32,
width: i32,
bottom: i32,
) -> Vec<GuiDraw> {
let mut out = vec![fill(
left,
bottom - CHART_HEIGHT,
left + width,
bottom,
BACKDROP,
)];
let (mut sum, mut min, mut max) = (0i64, i64::from(i32::MAX), i64::from(i32::MIN));
let start = CAPACITY.saturating_sub((width - 2).max(0) as usize);
let count = samples.len().saturating_sub(start);
for i in 0..count {
let x = left + i as i32 + 1;
let index = start + i;
let value = self.aggregated(samples, index);
min = min.min(value);
max = max.max(value);
sum += value;
self.bars(&mut out, samples, bottom, x, index);
}
horizontal_line(
&mut out,
left,
left + width - 1,
bottom - CHART_HEIGHT,
WHITE,
);
horizontal_line(&mut out, left, left + width - 1, bottom - 1, WHITE);
vertical_line(&mut out, left, bottom - CHART_HEIGHT, bottom, WHITE);
vertical_line(
&mut out,
left + width - 1,
bottom - CHART_HEIGHT,
bottom,
WHITE,
);
if count > 0 {
let y = bottom - CHART_HEIGHT - 9;
let min_text = format!("{} min", self.display(min as f64));
let avg_text = format!("{} avg", self.display(sum as f64 / count as f64));
let max_text = format!("{} max", self.display(max as f64));
text(&mut out, min_text, left + 2, y, LABEL, true);
let avg_x = left + width / 2 - font.width(&avg_text) as i32 / 2;
text(&mut out, avg_text, avg_x, y, LABEL, true);
let max_x = left + width - font.width(&max_text) as i32 - 2;
text(&mut out, max_text, max_x, y, LABEL, true);
}
self.lines_and_labels(&mut out, font, left, width, bottom);
out
}
fn bars(
self,
out: &mut Vec<GuiDraw>,
samples: &SampleLogger,
bottom: i32,
x: i32,
index: usize,
) {
let value = samples.get(index, 0);
let height = self.height(value as f64);
out.push(fill(x, bottom - height, x + 1, bottom, self.color(value)));
if let Chart::Tps { .. } = self {
let method = samples.get(index, TICK_SERVER_METHOD);
let method_h = self.height(method as f64);
out.push(fill(x, bottom - method_h, x + 1, bottom, TICK_METHOD_COLOR));
let tasks = samples.get(index, SCHEDULED_TASKS);
let tasks_h = self.height(tasks as f64);
out.push(fill(
x,
bottom - method_h - tasks_h,
x + 1,
bottom - method_h,
TASK_COLOR,
));
let other = value - samples.get(index, IDLE) - method - tasks;
let other_h = self.height(other as f64);
out.push(fill(
x,
bottom - other_h - tasks_h - method_h,
x + 1,
bottom - tasks_h - method_h,
OTHER_COLOR,
));
}
}
/// `getValueForAggregation`: a tick's time without its idle part.
fn aggregated(self, samples: &SampleLogger, index: usize) -> i64 {
match self {
Chart::Tps { .. } => samples.get(index, 0) - samples.get(index, IDLE),
_ => samples.get(index, 0),
}
}
/// `extractAdditionalLinesAndLabels`.
fn lines_and_labels(
self,
out: &mut Vec<GuiDraw>,
font: &Font,
left: i32,
width: i32,
bottom: i32,
) {
match self {
Chart::Fps { framerate_limit } => {
shaded(out, font, "30 fps", left + 1, bottom - CHART_HEIGHT + 1);
shaded(out, font, "60 fps", left + 1, bottom - 30 + 1);
horizontal_line(out, left, left + width - 1, bottom - 30, WHITE);
if framerate_limit > 0 && framerate_limit <= 250 {
let y = bottom - self.height(1.0e9 / f64::from(framerate_limit)) - 1;
horizontal_line(out, left, left + width - 1, y, CYAN);
}
}
Chart::Tps { mspt } => {
let tps = 1000.0 / mspt;
shaded(
out,
font,
&format!("{tps:.1} tps"),
left + 1,
bottom - CHART_HEIGHT + 1,
);
}
Chart::Ping => shaded(out, font, "500 ms", left + 1, bottom - CHART_HEIGHT + 1),
Chart::Bandwidth => {
for bytes in [64.0, 1024.0, 16384.0] {
let y = bottom - bandwidth_height(bytes);
shaded(out, font, &bandwidth_display(bytes), left + 1, y + 1);
horizontal_line(out, left, left + width - 1, y, WHITE);
}
let y = bottom - bandwidth_height(MEGABYTE) + 1;
shaded(out, font, &bandwidth_display(MEGABYTE), left + 1, y);
}
}
}
/// `toDisplayString`.
fn display(self, sample: f64) -> String {
match self {
Chart::Fps { .. } | Chart::Tps { .. } => format!("{} ms", java_round(sample / 1.0e6)),
Chart::Ping => format!("{} ms", java_round(sample)),
Chart::Bandwidth => bandwidth_display(sample * 20.0),
}
}
/// `getSampleHeight`.
fn height(self, sample: f64) -> i32 {
match self {
Chart::Fps { .. } => java_round(sample / 1.0e6 * 60.0 / 33.333333333333336) as i32,
Chart::Tps { mspt } => java_round(sample / 1.0e6 * 60.0 / f64::from(mspt)) as i32,
Chart::Ping => java_round(sample * 60.0 / 500.0) as i32,
Chart::Bandwidth => bandwidth_height(sample * 20.0),
}
}
/// `getSampleColor`.
fn color(self, sample: i64) -> u32 {
let s = sample as f64;
match self {
Chart::Fps { .. } => gradient(s / 1.0e6, 0.0, GREEN, 28.0, YELLOW, 56.0, RED),
Chart::Tps { mspt } => {
let m = f64::from(mspt);
gradient(s / 1.0e6, m, GREEN, m * 1.125, YELLOW, m * 1.25, RED)
}
Chart::Ping => gradient(s, 0.0, GREEN, 250.0, YELLOW, 500.0, RED),
Chart::Bandwidth => gradient(s * 20.0, 0.0, CYAN, 8192.0, LAVENDER, 1.048576e7, RED),
}
}
}
/// `BandwidthDebugChart.toDisplayStringInternal`.
fn bandwidth_display(bytes_per_second: f64) -> String {
if bytes_per_second >= MEGABYTE {
format!("{:.1} MiB/s", bytes_per_second / MEGABYTE)
} else if bytes_per_second >= 1024.0 {
format!("{:.1} KiB/s", bytes_per_second / 1024.0)
} else {
format!("{} B/s", bytes_per_second.floor() as i64)
}
}
/// `BandwidthDebugChart.getSampleHeightInternal`: logarithmic, 1 MiB/s at
/// the top.
fn bandwidth_height(bytes_per_second: f64) -> i32 {
java_round((bytes_per_second + 1.0).ln() * 60.0 / MEGABYTE.ln()) as i32
}
/// `Math.round(double)`: halves round up.
fn java_round(v: f64) -> i64 {
(v + 0.5).floor() as i64
}
/// `AbstractDebugChart.getSampleColor(sample, min, minColor, mid, ...)`.
fn gradient(
sample: f64,
min: f64,
min_color: u32,
mid: f64,
mid_color: u32,
max: f64,
max_color: u32,
) -> u32 {
let sample = sample.clamp(min, max);
if sample < mid {
srgb_lerp(((sample - min) / (mid - min)) as f32, min_color, mid_color)
} else {
srgb_lerp(((sample - mid) / (max - mid)) as f32, mid_color, max_color)
}
}
/// `ARGB.srgbLerp`: each channel by `Mth.lerpInt`.
fn srgb_lerp(alpha: f32, p0: u32, p1: u32) -> u32 {
let channel = |shift: u32| {
let (a, b) = (((p0 >> shift) & 255) as i32, ((p1 >> shift) & 255) as i32);
(a + (alpha * (b - a) as f32).floor() as i32) as u32
};
argb(channel(24), channel(16), channel(8), channel(0))
}
/// `GuiGraphicsExtractor.horizontalLine`.
fn horizontal_line(out: &mut Vec<GuiDraw>, x0: i32, x1: i32, y: i32, color: u32) {
out.push(fill(x0.min(x1), y, x0.max(x1) + 1, y + 1, color));
}
/// `GuiGraphicsExtractor.verticalLine`.
fn vertical_line(out: &mut Vec<GuiDraw>, x: i32, y0: i32, y1: i32, color: u32) {
out.push(fill(x, y0.min(y1) + 1, x + 1, y0.max(y1), color));
}
fn text(out: &mut Vec<GuiDraw>, s: String, x: i32, y: i32, color: u32, shadow: bool) {
out.push(GuiDraw::Text {
text: Text::literal(s),
x,
y,
color,
shadow,
});
}
/// `extractStringWithShade`: a label on a dark backdrop, without shadow.
fn shaded(out: &mut Vec<GuiDraw>, font: &Font, s: &str, x: i32, y: i32) {
out.push(fill(x, y, x + font.width(s) as i32 + 1, y + 9, BACKDROP));
text(out, s.to_owned(), x + 1, y + 1, LABEL, false);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_logger_keeps_the_last_240_samples() {
let mut l = SampleLogger::default();
for i in 0..300 {
l.log(i);
}
assert_eq!(l.len(), CAPACITY);
assert_eq!(l.get(0, 0), 60);
assert_eq!(l.get(CAPACITY - 1, 0), 299);
}
#[test]
fn chart_toggles_show_the_overlay_and_exclude_each_other() {
let mut c = DebugCharts::default();
let mut overlay = false;
c.toggle_fps(&mut overlay);
assert!(overlay && c.fps);
c.toggle_network(&mut overlay);
assert!(c.network && !c.fps);
c.toggle_network(&mut overlay);
assert!(!c.network && overlay);
// With the overlay hidden, the key shows it again with the chart on.
c.toggle_fps(&mut overlay);
overlay = false;
c.toggle_fps(&mut overlay);
assert!(overlay && c.fps);
}
#[test]
fn ticks_and_bandwidth_log_their_differences() {
let mut c = DebugCharts::default();
let t0 = Instant::now();
c.log_tick(t0, Duration::from_millis(5));
c.log_tick(t0 + Duration::from_millis(50), Duration::from_millis(7));
assert_eq!(
c.tick_times.samples[1],
[50_000_000, 7_000_000, 0, 43_000_000]
);
c.log_bandwidth(1000);
c.log_bandwidth(1600);
assert_eq!(c.bandwidth.len(), 1);
assert_eq!(c.bandwidth.get(0, 0), 600);
}
#[test]
fn heights_match_vanilla_scales() {
let fps = Chart::Fps {
framerate_limit: 260,
};
// 33.3 ms (30 fps) fills the chart; 16.7 ms half of it.
assert_eq!(fps.height(33_333_333.0), 60);
assert_eq!(fps.height(16_666_667.0), 30);
let tps = Chart::Tps { mspt: 50.0 };
assert_eq!(tps.height(25e6), 30);
assert_eq!(Chart::Ping.height(250.0), 30);
// 1 MiB/s at the top; 52 bytes per tick is 1 KiB/s.
assert_eq!(bandwidth_height(MEGABYTE), 60);
assert_eq!(Chart::Bandwidth.height(51.2), 30);
}
#[test]
fn labels_match_vanilla_formats() {
assert_eq!(
Chart::Fps { framerate_limit: 0 }.display(16_500_000.0),
"17 ms"
);
assert_eq!(Chart::Ping.display(12.4), "12 ms");
assert_eq!(Chart::Bandwidth.display(3.0), "60 B/s");
assert_eq!(Chart::Bandwidth.display(100.0), "2.0 KiB/s");
assert_eq!(bandwidth_display(MEGABYTE), "1.0 MiB/s");
}
#[test]
fn colours_go_green_yellow_red() {
let fps = Chart::Fps { framerate_limit: 0 };
assert_eq!(fps.color(0), GREEN);
assert_eq!(fps.color(28_000_000), YELLOW);
assert_eq!(fps.color(100_000_000), RED);
// Halfway between green and yellow: Mth.lerpInt floors.
assert_eq!(fps.color(14_000_000), 0xFF7F_FF00);
let tps = Chart::Tps { mspt: 50.0 };
assert_eq!(tps.color(10_000_000), GREEN);
assert_eq!(Chart::Bandwidth.color(0), CYAN);
}
}
+19 -1
View File
@@ -948,6 +948,8 @@ struct Game<'a> {
/// KeyboardHandler's debug key state (game/debug_keys.rs), the physical
/// keys held, and the debug renderers F3+B and F3+G switch on.
debug_keys: horizon_client::debug_keys::DebugKeys,
/// The F3 overlay's charts (F3+2, F3+3).
debug_charts: horizon_client::debug_chart::DebugCharts,
show_hitboxes: bool,
show_chunk_borders: bool,
/// PauseScreen.showPauseMenu (false after F3+Escape).
@@ -2855,6 +2857,7 @@ impl Game<'_> {
}
fn tick(&mut self) -> Result<()> {
self.tick_debug_keys()?;
self.tick_debug_charts();
if self.remote.is_some() {
return self.tick_remote();
}
@@ -3645,7 +3648,7 @@ impl Game<'_> {
2 => "north (Towards negative Z)",
_ => "east (Towards positive X)",
};
let lines = [
let mut lines = vec![
format!("Horizon 26.3 (Rust) {:.0} fps", self.fps),
format!(
"C: {} chunks, {} sections drawn / {}",
@@ -3688,7 +3691,9 @@ impl Game<'_> {
self.world.day_time / 24000,
self.world.day_time % 24000
),
String::new(),
];
lines.extend(self.debug_chart_lines());
for (i, line) in lines.iter().enumerate() {
if line.is_empty() {
continue;
@@ -3711,6 +3716,15 @@ impl Game<'_> {
false,
);
}
let charts = self.debug_charts.draw(
&atlas.font,
w as i32,
h as i32,
self.settings.max_fps,
1000.0 / self.clock.rate(),
self.remote.is_some(),
);
hud_layout::paint(&mut batch, atlas, &charts);
}
if !self.ui.hide_gui
&& let Some((text, at)) = &self.message
@@ -6287,6 +6301,7 @@ impl Game<'_> {
self.save.flush();
self.session_lock = None;
horizon_client::crash::clear_detail("World");
self.debug_charts.reset();
self.streaming.paused = true;
self.world = World::new(self.res.facts.air, 0);
self.player = None;
@@ -9056,6 +9071,7 @@ impl ApplicationHandler for Game<'_> {
// In single player the tick is the integrated server's.
if self.remote.is_none() {
self.perf.tick(tick.elapsed().as_secs_f32() * 1000.0);
self.debug_charts.log_tick(tick, tick.elapsed());
}
self.tick_ambience();
self.tick_fov();
@@ -9142,6 +9158,7 @@ impl ApplicationHandler for Game<'_> {
}
if rendered {
self.frame_stats.record(elapsed.as_secs_f32() * 1000.0);
self.debug_charts.frame_times.log(elapsed.as_nanos() as i64);
if self.frames == 0 {
// The launcher times Play to this line (launcher/src/perf.rs).
log::info!(
@@ -9620,6 +9637,7 @@ pub fn run() -> Result<()> {
world_options: None,
friends: None,
debug_keys: Default::default(),
debug_charts: Default::default(),
show_hitboxes: false,
show_chunk_borders: false,
pause_menu: true,
+83 -4
View File
@@ -151,7 +151,7 @@ impl Game<'_> {
}
}
/// `Util.getMillis` (positive: KeyboardHandler treats 0 as unset).
fn debug_millis(&self) -> i64 {
pub(super) fn debug_millis(&self) -> i64 {
self.started.elapsed().as_millis() as i64 + 1_000_000
}
/// `KeyboardHandler.tick`: the debug crash countdown.
@@ -180,6 +180,79 @@ impl Game<'_> {
}
Ok(())
}
/// The network charts' samples each tick: `PingDebugMonitor.tick` while
/// they show, `BandwidthDebugMonitor.tick` always.
pub(super) fn tick_debug_charts(&mut self) {
let shown = self.debug && self.debug_charts.network;
let Some(r) = &self.remote else {
// The integrated server answers within the tick.
if shown {
self.debug_charts.ping.log(0);
}
return;
};
let received = r
.session
.counters
.lock()
.unwrap()
.as_ref()
.map(|c| c.bytes_in.load(std::sync::atomic::Ordering::Relaxed));
if let Some(total) = received {
self.debug_charts.log_bandwidth(total);
}
if shown {
use horizon_protocol::packets::{play::Serverbound, status::Ping};
let time = self.debug_millis();
self.send(Serverbound::PingRequest(Ping { time }));
}
}
/// The overlay's chart lines (`DebugScreenOverlay`'s "Debug charts").
pub(super) fn debug_chart_lines(&self) -> [String; 3] {
let keys = &self.settings.keys;
let modifier = keys.get("key.debug.modifier");
let key = |mapping: &str| {
let prefix = if modifier == "key.keyboard.unknown" {
String::new()
} else {
format!("{}+", keybinds::display_name(modifier).plain())
};
format!(
"[{prefix}{}]",
keybinds::display_name(keys.get(mapping)).plain()
)
};
let chart = |mapping: &str, name: &str, on: bool| {
format!(
"{} {name} {}",
key(mapping),
if on { "visible" } else { "hidden" }
)
};
let remote = self.remote.is_some();
let c = &self.debug_charts;
[
format!(
"Debug charts: {}; {};",
chart("key.debug.profilingChart", "Profiler", false),
chart(
"key.debug.fpsCharts",
if remote { "fps" } else { "fps + tps" },
c.fps
),
),
format!(
"{}; {}",
chart(
"key.debug.networkCharts",
if remote { "Bandwidth + Ping" } else { "Ping" },
c.network
),
chart("key.debug.lightmapTexture", "Lightmap", false),
),
format!("To edit: press {}", key("key.debug.debugOptions")),
]
}
/// `KeyMapping.getTranslatedKeyMessage`.
fn key_text(&self, mapping: &str) -> Text {
keybinds::display_name(self.settings.keys.get(mapping))
@@ -339,15 +412,21 @@ impl Game<'_> {
});
true
}
DebugAction::FpsCharts => {
self.debug_charts.toggle_fps(&mut self.debug);
true
}
DebugAction::NetworkCharts => {
self.debug_charts.toggle_network(&mut self.debug);
true
}
// Screens and overlays Horizon does not draw yet (the debug
// options screen, the profiler and its charts, the lightmap, the
// options screen, the profiler and its chart, the lightmap, the
// texture dump): the keys are used up as in vanilla.
DebugAction::DebugOptions
| DebugAction::DumpDynamicTextures
| DebugAction::Profiling
| DebugAction::ProfilingChart
| DebugAction::FpsCharts
| DebugAction::NetworkCharts
| DebugAction::LightmapTexture => true,
DebugAction::ShowHitboxes
| DebugAction::ShowChunkBorders
+6
View File
@@ -272,6 +272,7 @@ impl Game<'_> {
r.session.disconnect();
}
horizon_client::crash::clear_detail("Server");
self.debug_charts.reset();
self.tab_list.reset();
self.score.board = Default::default();
self.streaming = Streaming::new(Arc::new(NoTerrain), self.res.context.clone(), 1);
@@ -736,6 +737,11 @@ impl Game<'_> {
fn handle_packet(&mut self, packet: C) -> Result<()> {
match packet {
// PingDebugMonitor.onPongReceived.
C::PongResponse(p) => {
let delay = self.debug_millis() - p.time;
self.debug_charts.ping.log(delay);
}
C::Login(p) => {
if let Some(r) = self.remote.as_mut() {
r.online_mode = p.online_mode;
+1
View File
@@ -44,6 +44,7 @@ pub mod crash;
pub mod creative;
pub mod data_cmd;
pub mod datapack;
pub mod debug_chart;
pub mod debug_keys;
pub mod effects;
pub mod enchant;
@@ -1012,6 +1012,7 @@ packet_enum!(Clientbound, crate::ids::play::clientbound {
MerchantOffers(MerchantOffers) = MERCHANT_OFFERS,
OpenScreen(OpenScreen) = OPEN_SCREEN,
Ping(Ping) = PING,
PongResponse(crate::packets::status::Ping) = PONG_RESPONSE,
PlayerAbilities(PlayerAbilities) = PLAYER_ABILITIES,
PlayerChat(PlayerChat) = PLAYER_CHAT,
PlayerCombatKill(PlayerCombatKill) = PLAYER_COMBAT_KILL,
@@ -1199,6 +1200,7 @@ packet_enum!(Serverbound, crate::ids::play::serverbound {
MovePlayerRot(MovePlayerRot) = MOVE_PLAYER_ROT,
MovePlayerStatus(MovePlayerStatus) = MOVE_PLAYER_STATUS_ONLY,
PickItemFromBlock(PickItemFromBlock) = PICK_ITEM_FROM_BLOCK,
PingRequest(crate::packets::status::Ping) = PING_REQUEST,
PlayerAction(PlayerAction) = PLAYER_ACTION,
PlayerCommand(PlayerCommand) = PLAYER_COMMAND,
PlayerInput(PlayerInput) = PLAYER_INPUT,
@@ -1240,6 +1242,7 @@ mod tests {
fn clientbound_round_trips() {
let packets = vec![
Clientbound::BundleDelimiter(Empty),
Clientbound::PongResponse(crate::packets::status::Ping { time: 1_234_567 }),
Clientbound::SetBorderCenter(SetBorderCenter { x: 1.5, z: -2.0 }),
Clientbound::SetBorderLerpSize(SetBorderLerpSize {
old_size: 10.0,
@@ -1529,6 +1532,7 @@ mod tests {
#[test]
fn serverbound_round_trips() {
let packets = vec![
Serverbound::PingRequest(crate::packets::status::Ping { time: -7 }),
Serverbound::AcceptTeleportation(AcceptTeleportation {
id: 1,
x: 0.5,