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