]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/xonotic/serverlist.c
All new method of declaring and setting up categories
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / xonotic / serverlist.c
1 #ifdef INTERFACE
2 CLASS(XonoticServerList) EXTENDS(XonoticListBox)
3         METHOD(XonoticServerList, configureXonoticServerList, void(entity))
4         ATTRIB(XonoticServerList, rowsPerItem, float, 1)
5         METHOD(XonoticServerList, draw, void(entity))
6         METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
7         METHOD(XonoticServerList, clickListBoxItem, void(entity, float, vector))
8         METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
9         METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
10
11         ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
12
13         ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
14         ATTRIB(XonoticServerList, realUpperMargin, float, 0)
15         ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
16         ATTRIB(XonoticServerList, columnIconsSize, float, 0)
17         ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
18         ATTRIB(XonoticServerList, columnPingSize, float, 0)
19         ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
20         ATTRIB(XonoticServerList, columnNameSize, float, 0)
21         ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
22         ATTRIB(XonoticServerList, columnMapSize, float, 0)
23         ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
24         ATTRIB(XonoticServerList, columnTypeSize, float, 0)
25         ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
26         ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
27
28         ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
29         METHOD(XonoticServerList, setSelected, void(entity, float))
30         METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
31         ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
32         ATTRIB(XonoticServerList, filterShowFull, float, 1)
33         ATTRIB(XonoticServerList, filterString, string, string_null)
34         ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
35         ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
36         ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
37         ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
38         METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: 0 = just reparametrize, 1 = send new requests, 2 = clear
39         ATTRIB(XonoticServerList, needsRefresh, float, 1)
40         METHOD(XonoticServerList, focusEnter, void(entity))
41         METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
42         ATTRIB(XonoticServerList, sortButton1, entity, NULL)
43         ATTRIB(XonoticServerList, sortButton2, entity, NULL)
44         ATTRIB(XonoticServerList, sortButton3, entity, NULL)
45         ATTRIB(XonoticServerList, sortButton4, entity, NULL)
46         ATTRIB(XonoticServerList, sortButton5, entity, NULL)
47         ATTRIB(XonoticServerList, connectButton, entity, NULL)
48         ATTRIB(XonoticServerList, infoButton, entity, NULL)
49         ATTRIB(XonoticServerList, currentSortOrder, float, 0)
50         ATTRIB(XonoticServerList, currentSortField, float, -1)
51         ATTRIB(XonoticServerList, lastClickedServer, float, -1)
52         ATTRIB(XonoticServerList, lastClickedTime, float, 0)
53
54         ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
55
56         ATTRIB(XonoticServerList, seenIPv4, float, 0)
57         ATTRIB(XonoticServerList, seenIPv6, float, 0)
58 ENDCLASS(XonoticServerList)
59 entity makeXonoticServerList();
60
61 void ServerList_Connect_Click(entity btn, entity me);
62 void ServerList_ShowEmpty_Click(entity box, entity me);
63 void ServerList_ShowFull_Click(entity box, entity me);
64 void ServerList_Filter_Change(entity box, entity me);
65 void ServerList_Favorite_Click(entity btn, entity me);
66 void ServerList_Info_Click(entity btn, entity me);
67 void ServerList_Update_favoriteButton(entity btn, entity me);
68
69 #ifndef IMPLEMENTATION
70 float SLIST_FIELD_CNAME;
71 float SLIST_FIELD_PING;
72 float SLIST_FIELD_GAME;
73 float SLIST_FIELD_MOD;
74 float SLIST_FIELD_MAP;
75 float SLIST_FIELD_NAME;
76 float SLIST_FIELD_MAXPLAYERS;
77 float SLIST_FIELD_NUMPLAYERS;
78 float SLIST_FIELD_NUMHUMANS;
79 float SLIST_FIELD_NUMBOTS;
80 float SLIST_FIELD_PROTOCOL;
81 float SLIST_FIELD_FREESLOTS;
82 float SLIST_FIELD_PLAYERS;
83 float SLIST_FIELD_QCSTATUS;
84 float SLIST_FIELD_CATEGORY;
85 float SLIST_FIELD_ISFAVORITE;
86 float SLSF_DESCENDING = 1;
87 float SLSF_FAVORITES = 2;
88 float SLSF_CATEGORIES = 4;
89
90 #define CATEGORY_FIRST 1
91 #define MAX_CATEGORIES 9
92 entity categories[MAX_CATEGORIES];
93 float category_ent_count;
94
95 float category_name[MAX_CATEGORIES];
96 float category_item[MAX_CATEGORIES];
97 float totcat;
98
99 .string cat_name;
100 .string cat_string;
101 .string cat_override_string;
102 .float cat_override;
103
104 #define CATEGORIES \
105         SLIST_CATEGORY(CAT_FAVORITED,    "",            "",             _("Favorites")) \
106         SLIST_CATEGORY(CAT_RECOMMENDED,  "",            "CAT_SERVERS",  _("Recommended")) \
107         SLIST_CATEGORY(CAT_NORMAL,       "",            "CAT_SERVERS",  _("Normal Servers")) \
108         SLIST_CATEGORY(CAT_SERVERS,      "CAT_NORMAL",  "CAT_SERVERS",  _("Servers")) \
109         SLIST_CATEGORY(CAT_XPM,          "CAT_NORMAL",  "CAT_SERVERS",  _("Competitive Mode")) \
110         SLIST_CATEGORY(CAT_MODIFIED,     "",            "CAT_SERVERS",  _("Modified Servers")) \
111         SLIST_CATEGORY(CAT_OVERKILL,     "",            "CAT_SERVERS",  _("Overkill Mode")) \
112         SLIST_CATEGORY(CAT_MINSTAGIB,    "",            "CAT_SERVERS",  _("MinstaGib Mode")) \
113         SLIST_CATEGORY(CAT_DEFRAG,       "",            "CAT_SERVERS",  _("Defrag Mode"))
114         
115 void RegisterSLCategories_First()
116 {
117         /*notif_global_error = FALSE;
118
119         #ifdef SVQC
120         #define dedi (server_is_dedicated ? "a dedicated " : "")
121         #else
122         #define dedi ""
123         #endif
124         
125         print(sprintf("Beginning notification initialization on %s%s program...\n", dedi, PROGNAME));*/
126         
127         // maybe do another implementation of this with checksums? for now, we don't need versioning
128         /*if(autocvar_notification_version != NOTIF_VERSION)
129         {
130                 #ifdef CSQC
131                 if(autocvar_notification_version_mismatch_client_error)
132                 #else
133                 if(autocvar_notification_version_mismatch_server_error)
134                 #endif
135                         notif_global_error = TRUE;
136
137                 print(sprintf("^1NOTIFICATION VERSION MISMATCH: ^7program = %s, config = %d, code = %d.\n",
138                         PROGNAME, autocvar_notification_version, NOTIF_VERSION));
139         }*/
140 }
141
142 float Get_Cat_Num_FromString(string input)
143 {
144         float i;
145         for(i = 0; i < category_ent_count; ++i) { if(categories[i].cat_name == input) { return (i + 1); } }
146         print(sprintf("Get_Cat_Num_FromString('%s'): Improper category name!\n", input));
147         return 0;
148 }
149 entity Get_Cat_Ent(float catnum)
150 {
151         if((catnum > 0) && (catnum <= category_ent_count))
152         {
153                 return categories[catnum - 1];
154         }
155         else
156         {
157                 error(sprintf("Get_Cat_Ent(%d): Improper category number!\n", catnum));
158                 return world;
159         }
160 }
161
162 void RegisterSLCategories_Done()
163 {
164         float i, catnum;
165         string s;
166         for(i = 0; i < category_ent_count; ++i)
167         {
168                 s = categories[i].cat_override_string;
169                 if((s != "") && (s != categories[i].cat_name))
170                 {
171                         catnum = Get_Cat_Num_FromString(s);
172                         if(catnum)
173                         {
174                                 strunzone(categories[i].cat_override_string);
175                                 categories[i].cat_override = catnum;
176                                 continue;
177                         }
178                 }
179                 strunzone(categories[i].cat_override_string);
180                 categories[i].cat_override = 0;
181         }
182 }
183
184 var float autocvar_menu_serverlist_categories = TRUE;
185
186 // C is stupid, must use extra macro for concatenation
187 #define SLIST_ADD_CAT_CVAR(name,default) var string autocvar_menu_serverlist_categories_##name##_override = default;
188 #define SLIST_CATEGORY(name,enoverride,deoverride,string) \
189         SLIST_ADD_CAT_CVAR(name, enoverride) \
190         float name; \
191         void RegisterSLCategory_##name() \
192         { \
193                 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
194                 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
195                 entity cat = spawn(); \
196                 categories[name - 1] = cat; \
197                 cat.classname = "slist_category"; \
198                 cat.cat_name = strzone(#name); \
199                 cat.cat_override_string = strzone((autocvar_menu_serverlist_categories ? \
200                         autocvar_menu_serverlist_categories_##name##_override \
201                         : \
202                         deoverride)); \
203                 cat.cat_string = strzone(string); \
204         } \
205         ACCUMULATE_FUNCTION(RegisterSLCategories, RegisterSLCategory_##name);
206
207 ACCUMULATE_FUNCTION(RegisterSLCategories, RegisterSLCategories_First);
208 CATEGORIES
209 ACCUMULATE_FUNCTION(RegisterSLCategories, RegisterSLCategories_Done);
210 #undef SLIST_ADD_CAT_CVAR
211 #undef SLIST_CATEGORY
212
213 var float autocvar_menu_serverlist_purethreshold = 10;
214 var string autocvar_menu_serverlist_recommended = "76.124.107.5:26004";
215 #endif
216
217 #endif
218
219 #ifdef IMPLEMENTATION
220 float IsServerInList(string list, string srv)
221 {
222         string p;
223         float i, n;
224         if(srv == "")
225                 return FALSE;
226         srv = netaddress_resolve(srv, 26000);
227         if(srv == "")
228                 return FALSE;
229         p = crypto_getidfp(srv);
230         n = tokenize_console(list);
231         for(i = 0; i < n; ++i)
232         {
233                 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
234                 {
235                         if(p)
236                                 if(argv(i) == p)
237                                         return TRUE;
238                 }
239                 else
240                 {
241                         if(srv == netaddress_resolve(argv(i), 26000))
242                                 return TRUE;
243                 }
244         }
245         return FALSE;
246 }
247
248 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
249 #define IsRecommended(srv) IsServerInList(cvar_string("menu_serverlist_recommended"), srv) // todo: use update notification instead of cvar
250
251 float cat_checkoverride(float cat)
252 {
253         entity catent = Get_Cat_Ent(cat);
254         if(catent)
255         {
256                 if(catent.cat_override) { return catent.cat_override; }
257                 else { return cat; }
258         }
259         else
260         {
261                 error(sprintf("cat_checkoverride(%d): Improper category number!\n", cat));
262                 return cat;
263         }
264 }
265
266 float m_getserverlistentrycategory(float entry)
267 {
268         string s, k, v, modtype = "";
269         float j, m, impure;
270         s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
271         m = tokenizebyseparator(s, ":");
272         //typestr = "";
273         //if(m >= 2)
274         //{
275         //      typestr = argv(0);
276         //      versionstr = argv(1);
277         //}
278         //freeslots = -1;
279         //sflags = -1;
280         //modname = "";
281         impure = 0;
282         for(j = 2; j < m; ++j)
283         {
284                 if(argv(j) == "")
285                         break;
286                 k = substring(argv(j), 0, 1);
287                 v = substring(argv(j), 1, -1);
288                 if(k == "P") { impure = stof(v); }
289                 else if(k == "M") { modtype = strtolower(v); }
290         }
291
292         //print(sprintf("modtype = %s\n", modtype)); 
293
294         if(impure > autocvar_menu_serverlist_purethreshold) { impure = TRUE; }
295         else { impure = FALSE; }
296
297         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return cat_checkoverride(CAT_FAVORITED); }
298         if(IsRecommended(gethostcachestring(SLIST_FIELD_CNAME, entry))) { return cat_checkoverride(CAT_RECOMMENDED); }
299         else if(modtype != "xonotic")
300         {
301                 switch(modtype)
302                 {
303                         // old servers which don't report their mod name are considered modified now
304                         case "": { return cat_checkoverride(CAT_MODIFIED); }
305                         
306                         case "xpm": { return cat_checkoverride(CAT_XPM); } 
307                         case "minstagib": { return cat_checkoverride(CAT_MINSTAGIB); }
308                         case "overkill": { return cat_checkoverride(CAT_OVERKILL); }
309
310                         // "cts" is allowed as compat, xdf is replacement
311                         case "cts": 
312                         case "xdf": { return cat_checkoverride(CAT_DEFRAG); }
313                         
314                         //if(modname != "CTS")
315                         //if(modname != "NIX")
316                         //if(modname != "NewToys")
317                         
318                         default: { print(sprintf("Found strange mod type: %s\n", modtype)); return cat_checkoverride(CAT_MODIFIED); }
319                 }
320         }
321         else { return cat_checkoverride((impure ? CAT_MODIFIED : CAT_NORMAL)); }
322
323         // should never hit this point
324         error("wtf m_getserverlistentrycategory fail?");
325         return FALSE;
326 }
327
328 float XonoticServerList_MapItems(float num)
329 {
330         float i, n;
331
332         if not(totcat) { return num; } // there are no categories to process
333
334         for(i = 0, n = 1; n <= totcat; ++i, ++n)
335         {
336                 //print(sprintf("num: %d, i: %d, totcat: %d, category_item[i]: %d\n", num, i, totcat, category_item[i])); 
337                 if(category_item[i] == (num - i)) { /*print("inserting cat... \\/\n");*/ return -category_name[i]; }
338                 else if(n == totcat) { /*print("end item... \\/\n");*/ return (num - n); }
339                 else if((num - i) <= category_item[n]) { /*print("next item... \\/\n");*/ return (num - n); }
340         }
341
342         // should never hit this point
343         error("wtf XonoticServerList_MapItems fail?");
344         return FALSE;
345 }
346
347 void ServerList_UpdateFieldIDs()
348 {
349         SLIST_FIELD_CNAME = gethostcacheindexforkey( "cname" );
350         SLIST_FIELD_PING = gethostcacheindexforkey( "ping" );
351         SLIST_FIELD_GAME = gethostcacheindexforkey( "game" );
352         SLIST_FIELD_MOD = gethostcacheindexforkey( "mod" );
353         SLIST_FIELD_MAP = gethostcacheindexforkey( "map" );
354         SLIST_FIELD_NAME = gethostcacheindexforkey( "name" );
355         SLIST_FIELD_MAXPLAYERS = gethostcacheindexforkey( "maxplayers" );
356         SLIST_FIELD_NUMPLAYERS = gethostcacheindexforkey( "numplayers" );
357         SLIST_FIELD_NUMHUMANS = gethostcacheindexforkey( "numhumans" );
358         SLIST_FIELD_NUMBOTS = gethostcacheindexforkey( "numbots" );
359         SLIST_FIELD_PROTOCOL = gethostcacheindexforkey( "protocol" );
360         SLIST_FIELD_FREESLOTS = gethostcacheindexforkey( "freeslots" );
361         SLIST_FIELD_PLAYERS = gethostcacheindexforkey( "players" );
362         SLIST_FIELD_QCSTATUS = gethostcacheindexforkey( "qcstatus" );
363         SLIST_FIELD_CATEGORY = gethostcacheindexforkey( "category" );
364         SLIST_FIELD_ISFAVORITE = gethostcacheindexforkey( "isfavorite" );
365 }
366
367 void ToggleFavorite(string srv)
368 {
369         string s, s0, s1, s2, srv_resolved, p;
370         float i, n, f;
371         srv_resolved = netaddress_resolve(srv, 26000);
372         p = crypto_getidfp(srv_resolved);
373         s = cvar_string("net_slist_favorites");
374         n = tokenize_console(s);
375         f = 0;
376         for(i = 0; i < n; ++i)
377         {
378                 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
379                 {
380                         if(p)
381                                 if(argv(i) != p)
382                                         continue;
383                 }
384                 else
385                 {
386                         if(srv_resolved != netaddress_resolve(argv(i), 26000))
387                                 continue;
388                 }
389                 s0 = s1 = s2 = "";
390                 if(i > 0)
391                         s0 = substring(s, 0, argv_end_index(i - 1));
392                 if(i < n-1)
393                         s2 = substring(s, argv_start_index(i + 1), -1);
394                 if(s0 != "" && s2 != "")
395                         s1 = " ";
396                 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
397                 s = cvar_string("net_slist_favorites");
398                 n = tokenize_console(s);
399                 f = 1;
400                 --i;
401         }
402         
403         if(!f)
404         {
405                 s1 = "";
406                 if(s != "")
407                         s1 = " ";
408                 if(p)
409                         cvar_set("net_slist_favorites", strcat(s, s1, p));
410                 else
411                         cvar_set("net_slist_favorites", strcat(s, s1, srv));
412         }
413
414         resorthostcache();
415 }
416
417 void ServerList_Update_favoriteButton(entity btn, entity me)
418 {
419         if(IsFavorite(me.ipAddressBox.text))
420                 me.favoriteButton.setText(me.favoriteButton, _("Remove"));
421         else
422                 me.favoriteButton.setText(me.favoriteButton, _("Bookmark"));
423 }
424
425 entity makeXonoticServerList()
426 {
427         entity me;
428         me = spawnXonoticServerList();
429         me.configureXonoticServerList(me);
430         return me;
431 }
432 void XonoticServerList_configureXonoticServerList(entity me)
433 {
434         me.configureXonoticListBox(me);
435
436         ServerList_UpdateFieldIDs();
437
438         me.nItems = 0;
439 }
440 void XonoticServerList_setSelected(entity me, float i)
441 {
442         float save, num;
443         save = me.selectedItem;
444         SUPER(XonoticServerList).setSelected(me, i);
445         /*
446         if(me.selectedItem == save)
447                 return;
448         */
449         if(me.nItems == 0)
450                 return;
451         //if(XonoticServerList_MapItems(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT)) != me.nItems)
452         //      { error("^1XonoticServerList_setSelected(); ERROR: ^7Host cache viewcount mismatches nItems!\n"); return; } // sorry, it would be wrong
453
454         num = XonoticServerList_MapItems(me.selectedItem);
455         if(num >= 0)
456         {
457                 if(me.selectedServer)
458                         strunzone(me.selectedServer);
459                 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, num));
460
461                 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
462                 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
463                 me.ipAddressBoxFocused = -1;
464         }
465 }
466 void XonoticServerList_refreshServerList(entity me, float mode)
467 {
468         // 0: just reparametrize
469         // 1: also ask for new servers
470         // 2: clear
471         //print("refresh of type ", ftos(mode), "\n");
472         /* if(mode == 2) // borken
473         {
474                 // clear list
475                 localcmd("net_slist\n");
476                 me.needsRefresh = 1; // net_slist kills sort order, so we need to restore it later
477         }
478         else */
479         
480         float m, i, n;
481         float listflags = 0;
482         string s, typestr, modstr;
483         s = me.filterString;
484
485         m = strstrofs(s, ":", 0);
486         if(m >= 0)
487         {
488                 typestr = substring(s, 0, m);
489                 s = substring(s, m + 1, strlen(s) - m - 1);
490                 while(substring(s, 0, 1) == " ")
491                         s = substring(s, 1, strlen(s) - 1);
492         }
493         else
494                 typestr = "";
495
496         modstr = cvar_string("menu_slist_modfilter");
497
498         m = SLIST_MASK_AND - 1;
499         resethostcachemasks();
500
501         // ping: reject negative ping (no idea why this happens in the first place, engine bug)
502         sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
503
504         // show full button
505         if(!me.filterShowFull)
506         {
507                 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
508                 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
509         }
510
511         // show empty button
512         if(!me.filterShowEmpty)
513                 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
514
515         // gametype filtering
516         if(typestr != "")
517                 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
518
519         // mod filtering
520         if(modstr != "")
521         {
522                 if(substring(modstr, 0, 1) == "!")
523                         sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
524                 else
525                         sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
526         }
527
528         // server banning
529         n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
530         for(i = 0; i < n; ++i)
531                 if(argv(i) != "")
532                         sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
533
534         m = SLIST_MASK_OR - 1;
535         if(s != "")
536         {
537                 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
538                 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
539                 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
540                 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
541         }
542
543         // sorting flags
544         //listflags |= SLSF_FAVORITES;
545         listflags |= SLSF_CATEGORIES;
546         if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
547         sethostcachesort(me.currentSortField, listflags);
548         
549         resorthostcache();
550         if(mode >= 1) { refreshhostcache(); }
551 }
552 void XonoticServerList_focusEnter(entity me)
553 {
554         if(time < me.nextRefreshTime)
555         {
556                 //print("sorry, no refresh yet\n");
557                 return;
558         }
559         me.nextRefreshTime = time + 10;
560         me.refreshServerList(me, 1);
561 }
562
563 void XonoticServerList_draw(entity me)
564 {
565         float i, found, owned, num;
566
567         if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
568         {
569                 if(!me.needsRefresh)
570                         me.needsRefresh = 2;
571                 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
572         }
573
574         if(me.currentSortField == -1)
575         {
576                 me.setSortOrder(me, SLIST_FIELD_PING, +1);
577                 me.refreshServerList(me, 2);
578         }
579         else if(me.needsRefresh == 1)
580         {
581                 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
582         }
583         else if(me.needsRefresh == 2)
584         {
585                 me.needsRefresh = 0;
586                 me.refreshServerList(me, 0);
587         }
588
589         owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
590
591         for(i = 0; i < totcat; ++i) { category_name[i] = -1; category_item[i] = -1; }
592         totcat = 0;
593
594         float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
595         //float visible = floor(me.scrollPos / me.itemHeight);
596         me.nItems = itemcount;
597
598         float cat, x;
599         for(i = 0; i < itemcount; ++i)
600         {
601                 cat = gethostcachenumber(SLIST_FIELD_CATEGORY, i);
602                 if(cat)
603                 {
604                         if(totcat == 0)
605                         {
606                                 category_name[totcat] = cat;
607                                 category_item[totcat] = i;
608                                 ++totcat;
609                                 ++me.nItems;
610                         }
611                         else
612                         {
613                                 found = 0;
614                                 for(x = 0; x < totcat; ++x) { if(cat == category_name[x]) { found = 1; } }
615                                 if not(found)
616                                 {
617                                         category_name[totcat] = cat;
618                                         category_item[totcat] = i;
619                                         ++totcat;
620                                         ++me.nItems;
621                                 }
622                         }
623                 }
624         }
625
626         //print(sprintf("^1SERVERLIST_DRAW^7: servercount: %d, nitems: %d\n", itemcount, me.nItems));
627
628         me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
629         me.infoButton.disabled = ((me.nItems == 0) || !owned);
630         me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
631
632         found = 0;
633         if(me.selectedServer)
634         {
635                 for(i = 0; i < me.nItems; ++i)
636                 {
637                         num = XonoticServerList_MapItems(i);
638                         if(num >= 0)
639                         {
640                                 if(gethostcachestring(SLIST_FIELD_CNAME, num) == me.selectedServer)
641                                 {
642                                         if(i != me.selectedItem)
643                                         {
644                                                 me.lastClickedServer = -1;
645                                                 me.selectedItem = i;
646                                         }
647                                         found = 1;
648                                         break;
649                                 }
650                         }
651                 }
652         }
653         if(!found)
654         {
655                 if(me.nItems > 0)
656                 {
657                         if(me.selectedItem >= me.nItems) { me.selectedItem = me.nItems - 1; }
658                         if(me.selectedServer) { strunzone(me.selectedServer); }
659
660                         num = XonoticServerList_MapItems(me.selectedItem);
661                         if(num >= 0) { me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, num)); }
662                 }
663         }
664         
665         if(owned)
666         {
667                 if(me.selectedServer != me.ipAddressBox.text)
668                 {
669                         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
670                         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
671                         me.ipAddressBoxFocused = -1;
672                 }
673         }
674
675         if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
676         {
677                 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
678                         ServerList_Update_favoriteButton(NULL, me);
679                 me.ipAddressBoxFocused = me.ipAddressBox.focused;
680         }
681
682         SUPER(XonoticServerList).draw(me);
683 }
684 void ServerList_PingSort_Click(entity btn, entity me)
685 {
686         me.setSortOrder(me, SLIST_FIELD_PING, +1);
687 }
688 void ServerList_NameSort_Click(entity btn, entity me)
689 {
690         me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
691 }
692 void ServerList_MapSort_Click(entity btn, entity me)
693 {
694         me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
695 }
696 void ServerList_PlayerSort_Click(entity btn, entity me)
697 {
698         me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
699 }
700 void ServerList_TypeSort_Click(entity btn, entity me)
701 {
702         string s, t;
703         float i, m;
704         s = me.filterString;
705         m = strstrofs(s, ":", 0);
706         if(m >= 0)
707         {
708                 s = substring(s, 0, m);
709                 while(substring(s, m+1, 1) == " ") // skip spaces
710                         ++m;
711         }
712         else
713                 s = "";
714
715         for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
716         {
717                 t = MapInfo_Type_ToString(i);
718                 if(i > 1)
719                         if(t == "") // it repeats (default case)
720                         {
721                                 // no type was found
722                                 // choose the first one
723                                 s = MapInfo_Type_ToString(1);
724                                 break;
725                         }
726                 if(s == t)
727                 {
728                         // the type was found
729                         // choose the next one
730                         s = MapInfo_Type_ToString(i * 2);
731                         if(s == "")
732                                 s = MapInfo_Type_ToString(1);
733                         break;
734                 }
735         }
736
737         if(s != "")
738                 s = strcat(s, ":");
739         s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
740
741         me.controlledTextbox.setText(me.controlledTextbox, s);
742         me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
743         me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
744         //ServerList_Filter_Change(me.controlledTextbox, me);
745 }
746 void ServerList_Filter_Change(entity box, entity me)
747 {
748         if(me.filterString)
749                 strunzone(me.filterString);
750         if(box.text != "")
751                 me.filterString = strzone(box.text);
752         else
753                 me.filterString = string_null;
754         me.refreshServerList(me, 0);
755
756         me.ipAddressBox.setText(me.ipAddressBox, "");
757         me.ipAddressBox.cursorPos = 0;
758         me.ipAddressBoxFocused = -1;
759 }
760 void ServerList_ShowEmpty_Click(entity box, entity me)
761 {
762         box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
763         me.refreshServerList(me, 0);
764
765         me.ipAddressBox.setText(me.ipAddressBox, "");
766         me.ipAddressBox.cursorPos = 0;
767         me.ipAddressBoxFocused = -1;
768 }
769 void ServerList_ShowFull_Click(entity box, entity me)
770 {
771         box.setChecked(box, me.filterShowFull = !me.filterShowFull);
772         me.refreshServerList(me, 0);
773
774         me.ipAddressBox.setText(me.ipAddressBox, "");
775         me.ipAddressBox.cursorPos = 0;
776         me.ipAddressBoxFocused = -1;
777 }
778 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
779 {
780         if(me.currentSortField == fld)
781                 direction = -me.currentSortOrder;
782         me.currentSortOrder = direction;
783         me.currentSortField = fld;
784         me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
785         me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
786         me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
787         me.sortButton4.forcePressed = 0;
788         me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
789         me.selectedItem = 0;
790         if(me.selectedServer)
791                 strunzone(me.selectedServer);
792         me.selectedServer = string_null;
793         me.refreshServerList(me, 0);
794 }
795 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
796 {
797         vector originInLBSpace, sizeInLBSpace;
798         originInLBSpace = eY * (-me.itemHeight);
799         sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
800
801         vector originInDialogSpace, sizeInDialogSpace;
802         originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
803         sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
804
805         btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
806         btn.Container_size_x   =                         sizeInDialogSpace_x * theSize;
807         btn.setText(btn, theTitle);
808         btn.onClick = theFunc;
809         btn.onClickEntity = me;
810         btn.resized = 1;
811 }
812 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
813 {
814         SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
815
816         me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
817         me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
818         me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
819
820         me.columnIconsOrigin = 0;
821         me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
822         me.columnPingSize = me.realFontSize_x * 3;
823         me.columnMapSize = me.realFontSize_x * 10;
824         me.columnTypeSize = me.realFontSize_x * 4;
825         me.columnPlayersSize = me.realFontSize_x * 5;
826         me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
827         me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
828         me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
829         me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
830         me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
831         me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
832
833         me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
834         me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
835         me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
836         me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
837         me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
838
839         float f;
840         f = me.currentSortField;
841         if(f >= 0)
842         {
843                 me.currentSortField = -1;
844                 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
845         }
846 }
847 void ServerList_Connect_Click(entity btn, entity me)
848 {
849         if(me.ipAddressBox.text == "")
850                 localcmd("connect ", me.selectedServer, "\n");
851         else
852                 localcmd("connect ", me.ipAddressBox.text, "\n");
853 }
854 void ServerList_Favorite_Click(entity btn, entity me)
855 {
856         string ipstr;
857         ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
858         if(ipstr != "")
859         {
860                 ToggleFavorite(me.ipAddressBox.text);
861                 me.ipAddressBoxFocused = -1;
862         }
863 }
864 void ServerList_Info_Click(entity btn, entity me)
865 {
866         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, XonoticServerList_MapItems(me.selectedItem));
867         DialogOpenButton_Click(me, main.serverInfoDialog);
868 }
869 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
870 {
871         float num = XonoticServerList_MapItems(i);
872         if(num == me.lastClickedServer)
873                 if(time < me.lastClickedTime + 0.3)
874                 {
875                         // DOUBLE CLICK!
876                         ServerList_Connect_Click(NULL, me);
877                 }
878         me.lastClickedServer = num;
879         me.lastClickedTime = time;
880 }
881 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
882 {
883         // layout: Ping, Server name, Map name, NP, TP, MP
884         float p, q;
885         float isv4, isv6;
886         vector theColor;
887         float theAlpha;
888         float m, pure, freeslots, j, sflags;
889         string s, typestr, versionstr, k, v, modname;
890
891         float cache = XonoticServerList_MapItems(i);
892         //print(sprintf("time: %f, i: %d, cache: %d, nitems: %d\n", time, i, cache, me.nItems));
893         
894         if(cache < 0)
895         {
896                 entity catent = Get_Cat_Ent(-cache);
897                 if(catent) { draw_Text(me.realUpperMargin * eY + (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5) * eX, catent.cat_string, me.realFontSize, '1 1 1', SKINALPHA_TEXT, 0); return; }
898         }
899         
900         if(isSelected)
901                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
902
903         s = gethostcachestring(SLIST_FIELD_QCSTATUS, cache);
904         m = tokenizebyseparator(s, ":");
905         typestr = "";
906         if(m >= 2)
907         {
908                 typestr = argv(0);
909                 versionstr = argv(1);
910         }
911         freeslots = -1;
912         sflags = -1;
913         modname = "";
914         pure = 0;
915         for(j = 2; j < m; ++j)
916         {
917                 if(argv(j) == "")
918                         break;
919                 k = substring(argv(j), 0, 1);
920                 v = substring(argv(j), 1, -1);
921                 if(k == "P")
922                         pure = stof(v);
923                 else if(k == "S")
924                         freeslots = stof(v);
925                 else if(k == "F")
926                         sflags = stof(v);
927                 else if(k == "M")
928                         modname = v;
929         }
930
931 #ifdef COMPAT_NO_MOD_IS_XONOTIC
932         if(modname == "")
933                 modname = "Xonotic";
934 #endif
935
936         /*
937         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
938         s = gethostcachestring(SLIST_FIELD_MOD, cache);
939         if(s != "data")
940                 if(modname == "Xonotic")
941                         modname = s;
942         */
943
944         // list the mods here on which the pure server check actually works
945         if(modname != "Xonotic")
946         if(modname != "MinstaGib")
947         if(modname != "CTS")
948         if(modname != "NIX")
949         if(modname != "NewToys")
950                 pure = 0;
951
952         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, cache) <= 0)
953                 theAlpha = SKINALPHA_SERVERLIST_FULL;
954         else if(freeslots == 0)
955                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
956         else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, cache))
957                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
958         else
959                 theAlpha = 1;
960
961         p = gethostcachenumber(SLIST_FIELD_PING, cache);
962 #define PING_LOW 75
963 #define PING_MED 200
964 #define PING_HIGH 500
965         if(p < PING_LOW)
966                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
967         else if(p < PING_MED)
968                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
969         else if(p < PING_HIGH)
970         {
971                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
972                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
973         }
974         else
975         {
976                 theColor = eX;
977                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
978         }
979
980         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, cache))
981         {
982                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
983                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
984         }
985
986         s = gethostcachestring(SLIST_FIELD_CNAME, cache);
987
988         isv4 = isv6 = 0;
989         if(substring(s, 0, 1) == "[")
990         {
991                 isv6 = 1;
992                 me.seenIPv6 += 1;
993         }
994         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
995         {
996                 isv4 = 1;
997                 me.seenIPv4 += 1;
998         }
999
1000         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1001         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1002         {
1003                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1004                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1005         }
1006
1007         if(q == 1)
1008         {
1009                 if(cvar("crypto_aeslevel") >= 2)
1010                         q |= 4;
1011         }
1012         if(q == 2)
1013         {
1014                 if(cvar("crypto_aeslevel") >= 1)
1015                         q |= 4;
1016         }
1017         if(q == 3)
1018                 q = 5;
1019         else if(q >= 3)
1020                 q -= 2;
1021         // possible status:
1022         // 0: crypto off
1023         // 1: AES possible
1024         // 2: AES recommended but not available
1025         // 3: AES possible and will be used
1026         // 4: AES recommended and will be used
1027         // 5: AES required
1028
1029         {
1030                 vector iconSize = '0 0 0';
1031                 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1032                 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1033
1034                 vector iconPos = '0 0 0';
1035                 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1036                 iconPos_y = (1 - iconSize_y) * 0.5;
1037
1038                 string n;
1039
1040                 if not(me.seenIPv4 && me.seenIPv6)
1041                 {
1042                         iconPos_x += iconSize_x * 0.5;
1043                 }
1044                 else if(me.seenIPv4 && me.seenIPv6)
1045                 {
1046                         n = string_null;
1047                         if(isv6)
1048                                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1049                         else if(isv4)
1050                                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1051                         if(n)
1052                                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1053                         iconPos_x += iconSize_x;
1054                 }
1055
1056                 if(q > 0)
1057                 {
1058                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1059                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1060                 }
1061                 iconPos_x += iconSize_x;
1062
1063                 if(modname == "Xonotic")
1064                 {
1065                         if(pure == 0)
1066                         {
1067                                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1068                                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1069                         }
1070                 }
1071                 else
1072                 {
1073                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1074                         if(draw_PictureSize(n) == '0 0 0')
1075                                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1076                         if(pure == 0)
1077                                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1078                         else
1079                                 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1080                 }
1081                 iconPos_x += iconSize_x;
1082
1083                 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1084                 {
1085                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1086                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1087                 }
1088                 iconPos_x += iconSize_x;
1089         }
1090
1091         s = ftos(p);
1092         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1093         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, cache), me.columnNameSize, 0, me.realFontSize);
1094         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1095         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, cache), me.columnMapSize, 0, me.realFontSize);
1096         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1097         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1098         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1099         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, cache)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, cache)));
1100         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1101 }
1102
1103 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1104 {
1105         float i = XonoticServerList_MapItems(me.selectedItem);
1106         vector org, sz;
1107
1108         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1109         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1110
1111         if(scan == K_ENTER || scan == K_KP_ENTER)
1112         {
1113                 ServerList_Connect_Click(NULL, me);
1114                 return 1;
1115         }
1116         else if(scan == K_MOUSE2 || scan == K_SPACE)
1117         {
1118                 if((me.nItems != 0) && (i >= 0))
1119                 {
1120                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1121                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1122                         return 1;
1123                 }
1124                 return 0;
1125         }
1126         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1127         {
1128                 if((me.nItems != 0) && (i >= 0))
1129                 {
1130                         ToggleFavorite(me.selectedServer);
1131                         me.ipAddressBoxFocused = -1;
1132                         return 1;
1133                 }
1134                 return 0;
1135         }
1136         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1137                 return 1;
1138         else if(!me.controlledTextbox)
1139                 return 0;
1140         else
1141                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1142 }
1143 #endif