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