Skip to main content

pumpkinplus/modules/mechanics/server/
tablist.rs

1//! Tablist module - custom header/footer with dynamic placeholders.
2//!
3//! ## Configuration
4//!
5//! | Field     | Default | Description                                                          |
6//! |-----------|---------|----------------------------------------------------------------------|
7//! | `enabled` | `false` | Whether this module is active                                        |
8//! | `header`  | `""`    | Header text. Supports placeholders and Minecraft formatting codes  |
9//! | `footer`  | `""`    | Footer text. Supports placeholders and Minecraft formatting codes  |
10//!
11//! ## Placeholders
12//!
13//! | Placeholder | Description                    | Example Output |
14//! |-------------|--------------------------------|----------------|
15//! | `{player}`  | Current player's name            | `Notch`        |
16//! | `{online}`  | Number of online players         | `42`           |
17//! | `{tps}`     | Server TPS (ticks per second)    | `20.0`         |
18//! | `{mspt}`    | Milliseconds per tick            | `5.2`          |
19
20use crate::config::ConfigManager;
21use crate::mechanics::mechanic::Mechanic;
22use pumpkin_plugin_api::events::{
23    EventData, EventHandler, EventPriority, PlayerJoinEvent, PlayerLeaveEvent,
24};
25use pumpkin_plugin_api::player::Player;
26use pumpkin_plugin_api::text::TextComponent;
27use pumpkin_plugin_api::{Context, Server};
28use serde::{Deserialize, Serialize};
29
30/// Handles tab-list mechanics, including custom messages.
31#[derive(Default)]
32pub struct Tablist;
33
34impl Mechanic for Tablist {
35    fn enabled(&self) -> bool {
36        ConfigManager::get()
37            .map(|cm| cm.get_config::<TablistConfig>().enabled)
38            .unwrap_or(true)
39    }
40
41    fn events(&self, context: &Context) {
42        context
43            .register_event_handler::<PlayerJoinEvent, _>(Tablist, EventPriority::Normal, true)
44            .expect("failed to register tablist event handler");
45        context
46            .register_event_handler::<PlayerLeaveEvent, _>(Tablist, EventPriority::Normal, true)
47            .expect("failed to register tablist leave event handler");
48    }
49}
50
51impl Tablist {
52    /// Replaces `{player}`, `{online}`, `{tps}`, and `{mspt}` placeholders in
53    /// a string with live server and player data.
54    fn replace_placeholders(text: &str, server: &Server, player: &Player) -> String {
55        let player_name = player.get_display_name().get_text();
56        let online = server.get_player_count();
57        let tps = server.get_tps();
58        let mspt = server.get_mspt();
59
60        text.replace("{player}", &player_name)
61            .replace("{online}", &online.to_string())
62            .replace("{tps}", &format!("{:.1}", tps))
63            .replace("{mspt}", &format!("{:.1}", mspt))
64    }
65
66    /// Applies the configured header and footer to a single player,
67    /// resolving placeholders for that player.
68    fn update_tablist_for_player(config: &TablistConfig, server: &Server, player: &Player) {
69        let header = Self::replace_placeholders(&config.header, server, player);
70        let footer = Self::replace_placeholders(&config.footer, server, player);
71        player
72            .set_tab_list_header_footer(TextComponent::text(&header), TextComponent::text(&footer));
73    }
74
75    /// Refreshes the tab list header and footer for every online player.
76    fn update_tablist_for_all_players(server: &Server) {
77        let config: TablistConfig = ConfigManager::get()
78            .map(|cm| cm.get_config())
79            .unwrap_or_default();
80
81        if !config.enabled {
82            return;
83        }
84
85        for player in server.get_all_players() {
86            Self::update_tablist_for_player(&config, server, &player);
87        }
88    }
89}
90
91impl EventHandler<PlayerJoinEvent> for Tablist {
92    fn handle(
93        &self,
94        server: Server,
95        event: EventData<PlayerJoinEvent>,
96    ) -> EventData<PlayerJoinEvent> {
97        let config: TablistConfig = ConfigManager::get()
98            .map(|cm| cm.get_config())
99            .unwrap_or_default();
100
101        if !self.enabled() {
102            return event;
103        }
104
105        Self::update_tablist_for_player(&config, &server, &event.player);
106
107        for player in server.get_all_players() {
108            if player.get_display_name().get_text() != event.player.get_display_name().get_text() {
109                Self::update_tablist_for_player(&config, &server, &player);
110            }
111        }
112
113        event
114    }
115}
116
117impl EventHandler<PlayerLeaveEvent> for Tablist {
118    fn handle(
119        &self,
120        server: Server,
121        event: EventData<PlayerLeaveEvent>,
122    ) -> EventData<PlayerLeaveEvent> {
123        if !self.enabled() {
124            return event;
125        }
126
127        Self::update_tablist_for_all_players(&server);
128
129        event
130    }
131}
132
133/// Configuration for the tablist mechanics module.
134#[derive(Debug, Default, Clone, Serialize, Deserialize)]
135pub struct TablistConfig {
136    /// Whether this module is active.
137    pub enabled: bool,
138    /// Header text displayed at the top of the tab list. Supports Minecraft formatting codes. Leave empty to disable.
139    pub header: String,
140    /// Footer text displayed at the bottom of the tab list. Supports Minecraft formatting codes. Leave empty to disable.
141    pub footer: String,
142}