pumpkinplus/modules/mechanics/player/
nickname.rs1use crate::{PLUGIN_ID, config::ConfigManager, mechanics::mechanic::Mechanic};
24use pumpkin_plugin_api::{
25 Context, Server,
26 command::{Command, CommandError, CommandNode, CommandSender, ConsumedArgs},
27 command_wit::{ArgumentType, StringType},
28 commands::CommandHandler,
29 events::{EventData, EventHandler, EventPriority, PlayerJoinEvent},
30 player::Player,
31 text::TextComponent,
32};
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::collections::HashSet;
36use std::fs;
37use std::path::PathBuf;
38use tracing::error;
39
40use std::cell::RefCell;
41
42thread_local! {
43 static DATA_FOLDER: RefCell<Option<String>> = const { RefCell::new(None) };
44}
45
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
48struct NicknamesStore {
49 nicknames: HashMap<String, String>,
50}
51
52impl NicknamesStore {
53 fn path(data_folder: &str) -> PathBuf {
55 PathBuf::from(data_folder.trim_start_matches("./")).join("nicknames.json")
56 }
57
58 fn load(data_folder: &str) -> Self {
59 let path = Self::path(data_folder);
60 if path.exists() {
61 fs::read_to_string(&path)
62 .ok()
63 .and_then(|s| serde_json::from_str(&s).ok())
64 .unwrap_or_default()
65 } else {
66 Self::default()
67 }
68 }
69
70 fn save(&self, data_folder: &str) {
71 let path = Self::path(data_folder);
72 if let Some(parent) = path.parent() {
73 fs::create_dir_all(parent).ok();
74 }
75 fs::write(
76 &path,
77 serde_json::to_string_pretty(self).unwrap_or_default(),
78 )
79 .inspect_err(|e| error!("Failed to write nicknames.json: {}", e))
80 .ok();
81 }
82
83 fn get(&self, uuid: &str) -> Option<&String> {
84 self.nicknames.get(uuid)
85 }
86
87 fn set(&mut self, data_folder: &str, uuid: &str, nickname: String) {
88 if nickname.is_empty() {
89 self.nicknames.remove(uuid);
90 } else {
91 self.nicknames.insert(uuid.to_string(), nickname);
92 }
93 self.save(data_folder);
94 }
95}
96
97#[derive(Default)]
99pub struct Nickname;
100
101impl Mechanic for Nickname {
102 fn enabled(&self) -> bool {
103 ConfigManager::get()
104 .map(|cm| cm.get_config::<NicknameConfig>().enabled)
105 .unwrap_or(false)
106 }
107
108 fn cmds(&self) -> Vec<Command> {
109 let command = Command::new(
110 &["nickname".to_string(), "nick".to_string()],
111 "Set or remove your nickname",
112 );
113 command.then(
115 CommandNode::argument("name", &ArgumentType::String(StringType::Greedy))
116 .execute(NicknameExecutor),
117 );
118 command.then(CommandNode::literal("clear").execute(NicknameExecutor));
120 vec![command]
121 }
122
123 fn perms(&self) -> HashSet<String> {
124 HashSet::from([format!("{}:command.nickname", PLUGIN_ID)])
125 }
126
127 fn events(&self, context: &Context) {
128 DATA_FOLDER.with(|f| {
129 *f.borrow_mut() = Some(context.get_data_folder().to_string());
130 });
131
132 context
133 .register_event_handler::<PlayerJoinEvent, _>(Nickname, EventPriority::Normal, true)
134 .expect("failed to register nickname join event handler");
135 }
136}
137
138struct NicknameExecutor;
139
140impl CommandHandler for NicknameExecutor {
141 fn handle(
142 &self,
143 sender: CommandSender,
144 _server: Server,
145 args: ConsumedArgs,
146 ) -> Result<i32, CommandError> {
147 let player = sender.as_player().ok_or(CommandError::PermissionDenied)?;
148 let uuid = format!("{}-{}", player.get_id().high, player.get_id().low);
149
150 let data_folder = DATA_FOLDER.with(|f| f.borrow().clone().unwrap_or_default());
151 let mut store = NicknamesStore::load(&data_folder);
152
153 let arg = args.get_value("name");
155 let nickname = match arg {
156 pumpkin_plugin_api::command_wit::Arg::Simple(name) => name,
157 _ => {
158 store.set(&data_folder, &uuid, String::new());
159 update_player(&player, None);
160 sender.send_message(TextComponent::text("Nickname cleared."));
161 return Ok(0);
162 }
163 };
164
165 let trimmed = nickname.trim();
166 if trimmed.is_empty() {
167 store.set(&data_folder, &uuid, String::new());
168 update_player(&player, None);
169 sender.send_message(TextComponent::text("Nickname cleared."));
170 } else {
171 store.set(&data_folder, &uuid, trimmed.to_string());
172 update_player(&player, Some(trimmed));
173 sender.send_message(TextComponent::text(&format!(
174 "Nickname updated to: {}",
175 trimmed
176 )));
177 }
178
179 Ok(0)
180 }
181}
182
183impl EventHandler<PlayerJoinEvent> for Nickname {
184 fn handle(
185 &self,
186 _server: Server,
187 event: EventData<PlayerJoinEvent>,
188 ) -> EventData<PlayerJoinEvent> {
189 if !self.enabled() {
190 return event;
191 }
192
193 let data_folder = DATA_FOLDER.with(|f| f.borrow().clone().unwrap_or_default());
194 let store = NicknamesStore::load(&data_folder);
195 let uuid = format!(
196 "{}-{}",
197 event.player.get_id().high,
198 event.player.get_id().low
199 );
200
201 if let Some(nickname) = store.get(&uuid) {
202 update_player(&event.player, Some(nickname));
203 }
204
205 event
206 }
207}
208
209fn update_player(player: &Player, nickname: Option<&str>) {
211 let display = match nickname {
212 Some(name) => TextComponent::text(name),
213 None => TextComponent::text(&player.get_name()),
214 };
215
216 let tab_list = match nickname {
217 Some(name) => TextComponent::text(name),
218 None => TextComponent::text(&player.get_name()),
219 };
220
221 player.set_display_name(display);
222 player.set_tab_list_name(Some(tab_list));
223}
224
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct NicknameConfig {
228 pub enabled: bool,
230}