classicube_helpers\tab_list/
mod.rs1mod entry;
2use std::{
3 cell::RefCell,
4 collections::HashMap,
5 rc::{Rc, Weak},
6};
7
8use classicube_sys::{TabList, TABLIST_MAX_NAMES};
9use tracing::warn;
10
11pub use self::entry::TabListEntry;
12use crate::{
13 callback_handler::CallbackHandler,
14 events::{net, tab_list},
15};
16
17#[derive(Default)]
19pub struct TabList {
20 entries: Rc<RefCell<HashMap<u8, Rc<TabListEntry>>>>,
21
22 #[allow(clippy::type_complexity)]
23 added_callbacks: Rc<RefCell<CallbackHandler<(u8, Weak<TabListEntry>)>>>,
24 #[allow(dead_code)]
25 added_handler: tab_list::AddedEventHandler,
26
27 #[allow(clippy::type_complexity)]
28 changed_callbacks: Rc<RefCell<CallbackHandler<(u8, Weak<TabListEntry>)>>>,
29 #[allow(dead_code)]
30 changed_handler: tab_list::ChangedEventHandler,
31
32 removed_callbacks: Rc<RefCell<CallbackHandler<u8>>>,
33 #[allow(dead_code)]
34 removed_handler: tab_list::RemovedEventHandler,
35
36 disconnected_handler: net::DisconnectedEventHandler,
37}
38
39impl TabList {
40 #[must_use]
42 pub fn new() -> Self {
43 let entries = HashMap::with_capacity(256);
44 let entries = Rc::new(RefCell::new(entries));
45
46 let added_callbacks = Rc::new(RefCell::new(CallbackHandler::new()));
47 let mut added_handler = tab_list::AddedEventHandler::new();
48 {
49 let entries = entries.clone();
50 let added_callbacks = added_callbacks.clone();
51 added_handler.on(move |tab_list::AddedEvent { id }| {
52 let id = *id;
53 let entry = Rc::new(match unsafe { TabListEntry::from_id(id) } {
54 None => {
55 warn!(?id, "AddedEvent TabListEntry::from_id returned None");
56 return;
57 }
58 Some(entry) => entry,
59 });
60 let weak = Rc::downgrade(&entry);
61
62 {
63 let mut entries = entries.borrow_mut();
64 entries.insert(id, entry);
65 }
66
67 let mut added_callbacks = added_callbacks.borrow_mut();
68 added_callbacks.handle_event(&(id, weak));
69 });
70 }
71
72 let changed_callbacks = Rc::new(RefCell::new(CallbackHandler::new()));
73 let mut changed_handler = tab_list::ChangedEventHandler::new();
74 {
75 let entries = entries.clone();
76 let changed_callbacks = changed_callbacks.clone();
77 changed_handler.on(move |tab_list::ChangedEvent { id }| {
78 let id = *id;
79
80 let entry = Rc::new(match unsafe { TabListEntry::from_id(id) } {
81 None => {
82 warn!(?id, "ChangedEvent TabListEntry::from_id returned None");
83 return;
84 }
85 Some(entry) => entry,
86 });
87 let weak = Rc::downgrade(&entry);
88 {
89 let mut entries = entries.borrow_mut();
90 entries.entry(id).or_insert(entry);
91 }
92
93 let mut changed_callbacks = changed_callbacks.borrow_mut();
94 changed_callbacks.handle_event(&(id, weak));
95 });
96 }
97
98 let removed_callbacks = Rc::new(RefCell::new(CallbackHandler::new()));
99 let mut removed_handler = tab_list::RemovedEventHandler::new();
100 {
101 let entries = entries.clone();
102 let removed_callbacks = removed_callbacks.clone();
103 removed_handler.on(move |tab_list::RemovedEvent { id }| {
104 {
105 let mut entries = entries.borrow_mut();
106 entries.remove(id);
107 }
108
109 let mut removed_callbacks = removed_callbacks.borrow_mut();
110 removed_callbacks.handle_event(id);
111 });
112 }
113
114 let mut disconnected_handler = net::DisconnectedEventHandler::new();
115 {
116 let entries = entries.clone();
117 disconnected_handler.on(move |_| {
118 let mut entries = entries.borrow_mut();
119 entries.clear();
120 });
121 }
122
123 let mut s = Self {
124 entries,
125 added_callbacks,
126 added_handler,
127 changed_callbacks,
128 changed_handler,
129 removed_callbacks,
130 removed_handler,
131 disconnected_handler,
132 };
133
134 s.update_to_real_entries();
135
136 s
137 }
138
139 fn update_to_real_entries(&mut self) {
140 let mut entries = self.entries.borrow_mut();
141 entries.clear();
142
143 for id in 0..TABLIST_MAX_NAMES {
144 unsafe {
145 if TabList.NameOffsets[id as usize] != 0 {
146 if let Some(entry) = TabListEntry::from_id(u8::try_from(id).unwrap()) {
147 entries.insert(u8::try_from(id).unwrap(), Rc::new(entry));
148 }
149 }
150 }
151 }
152 }
153
154 pub fn on_added<F>(&mut self, callback: F)
155 where
156 F: FnMut(&(u8, Weak<TabListEntry>)),
157 F: 'static,
158 {
159 let mut added_callbacks = self.added_callbacks.borrow_mut();
160 added_callbacks.on(callback);
161 }
162
163 pub fn on_changed<F>(&mut self, callback: F)
164 where
165 F: FnMut(&(u8, Weak<TabListEntry>)),
166 F: 'static,
167 {
168 let mut changed_callbacks = self.changed_callbacks.borrow_mut();
169 changed_callbacks.on(callback);
170 }
171
172 pub fn on_removed<F>(&mut self, callback: F)
173 where
174 F: FnMut(&u8),
175 F: 'static,
176 {
177 let mut removed_callbacks = self.removed_callbacks.borrow_mut();
178 removed_callbacks.on(callback);
179 }
180
181 pub fn on_disconnected<F>(&mut self, callback: F)
182 where
183 F: FnMut(&net::DisconnectedEvent),
184 F: 'static,
185 {
186 self.disconnected_handler.on(callback);
187 }
188
189 fn best_match(&self, search: &str) -> Option<Weak<TabListEntry>> {
190 let entries = self.entries.borrow();
192 let mut positions: Vec<_> = entries
193 .values()
194 .filter_map(|entry| {
195 let nick_name = entry.get_nick_name().replace(" &7(AFK)", "");
196
197 let search = remove_color(search);
221 let real_nick = remove_color(&nick_name);
222
223 let search: String = search.chars().rev().collect();
225 let real_nick: String = real_nick.chars().rev().collect();
226
227 search.find(&real_nick).map(|pos| (entry, nick_name, pos))
228 })
229 .collect();
230
231 positions.sort_unstable_by(|(_entry1, name1, pos1), (_entry2, name2, pos2)| {
240 pos1.partial_cmp(pos2)
241 .unwrap()
242 .then_with(|| name2.len().partial_cmp(&name1.len()).unwrap())
243 });
244
245 positions
246 .first()
247 .map(|(entry, _name, _pos)| Rc::downgrade(*entry))
248 }
249
250 #[must_use]
251 pub fn find_entry_by_nick_name(&self, search: &str) -> Option<Weak<TabListEntry>> {
252 let entries = self.entries.borrow();
253 let option = entries.values().find(|entry| {
254 let nick_name = entry.get_nick_name().replace(" &7(AFK)", "");
257 nick_name == search ||
258 remove_color(&nick_name) == remove_color(search)
260 });
261
262 if let Some(a) = option {
263 Some(Rc::downgrade(a))
264 } else {
265 let option = entries.values().find(|entry| {
266 let real_name = entry.get_real_name().replace(" &7(AFK)", "");
269 real_name == search ||
270 remove_color(&real_name) == remove_color(search)
272 });
273
274 if let Some(a) = option {
275 Some(Rc::downgrade(a))
276 } else {
277 self.best_match(search)
281 }
282 }
283 }
284
285 #[must_use]
286 pub fn get(&self, id: u8) -> Option<Weak<TabListEntry>> {
287 let entries = self.entries.borrow();
288 let entry = entries.get(&id)?;
289 Some(Rc::downgrade(entry))
290 }
291
292 #[must_use]
293 pub fn get_all(&self) -> Vec<(u8, Weak<TabListEntry>)> {
294 let entries = self.entries.borrow();
295 entries
296 .values()
297 .map(|entry| (entry.get_id(), Rc::downgrade(entry)))
298 .collect::<Vec<_>>()
299 }
300}
301
302#[cfg(all(windows, not(feature = "ci")))]
303#[ignore]
304#[test]
305fn test_find_entry_by_nick_name() {
306 use classicube_sys::*;
307
308 let tab_list = TabList::new();
309
310 let pairs = [
311 ("goodlyay", "&a&f�&a Goodly"),
312 ("SpiralP", "&u&bs&fp&6i&fr&ba"),
313 ("", "&9&9\u{b}&9 &rSp&9&3a&9c&1e"),
314 ("SpiralP2", "&7SpiralP2 &f0"),
315 ];
316
317 for (i, (real_nick, nick_name)) in pairs.iter().enumerate() {
318 unsafe {
319 let player = OwnedString::new(*real_nick);
320 let list = OwnedString::new(*nick_name);
321 let group = OwnedString::new("group");
322 TabList_Set(
323 i as _,
324 player.as_cc_string(),
325 list.as_cc_string(),
326 group.as_cc_string(),
327 0,
328 );
329
330 Event_RaiseInt(&mut TabListEvents.Added, i as _);
331 }
332 }
333
334 println!(
335 "{:#?}",
336 tab_list
337 .find_entry_by_nick_name("&7<Local>&u&u[&9Agg.Boo&9&u] &bs&fp&6i&fr&ba")
338 .unwrap()
339 );
340
341 println!(
342 "{:#?}",
343 tab_list
344 .find_entry_by_nick_name("&9[&b� &rBi&9r&1d &b�&9] &9\u{b}&9 &rSp&3a&9c&1e")
345 .unwrap()
346 );
347}
348
349#[cfg(all(windows, not(feature = "ci")))]
350#[ignore]
351#[test]
352fn test_match_names() {
353 let search = "NotSpiralP";
354 let names = vec!["hello", "SpiralP", "SpiralP2", "NotSpiralP", "SpiralP2"];
355
356 let mut positions: Vec<_> = names
357 .iter()
358 .filter_map(|nick_name| {
359 fn remove_beginning_color(s: &str) -> &str {
360 if s.len() >= 2 && s.starts_with('&') {
361 let (_color, s) = s.split_at(2);
362 s
363 } else {
364 s
365 }
366 }
367
368 let search = remove_beginning_color(search);
369 let real_nick = remove_beginning_color(nick_name);
370
371 let search: String = search.chars().rev().collect();
373 let real_nick: String = real_nick.chars().rev().collect();
374
375 search.find(&real_nick).map(|pos| (nick_name, pos))
376 })
377 .collect();
378
379 positions.sort_unstable_by(|(name1, pos1), (name2, pos2)| {
380 pos1.partial_cmp(pos2)
381 .unwrap()
382 .then_with(|| name2.len().partial_cmp(&name1.len()).unwrap())
383 });
384 println!("{:#?}", positions);
385}
386
387pub fn remove_color<T: AsRef<str>>(text: T) -> String {
388 let mut found_ampersand = false;
389
390 text.as_ref()
391 .chars()
392 .filter(|&c| {
393 if c == '&' {
394 found_ampersand = true;
396 false
397 } else if found_ampersand {
398 found_ampersand = false;
399 false
400 } else {
401 true
402 }
403 })
404 .collect()
405}
406
407#[test]
408fn test_remove_color() {
409 let pairs = [
410 ("SpiralP", "SpiralP"),
411 ("SpiralP", "SpiralP"),
412 ("SpiralP", "SpiralP"),
413 ("SpiralP", "&bS&fp&6i&fr&balP"),
414 ];
415
416 for (a, b) in &pairs {
417 assert_eq!(remove_color(b), *a);
418 }
419}