]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/xonotic/serverlist.qc
Merge branch 'terencehill/menu_remove_tab_title' of git://de.git.xonotic.org/xonotic...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / xonotic / serverlist.qc
1 #ifndef SERVERLIST_H
2 #define SERVERLIST_H
3 #include "listbox.qc"
4 CLASS(XonoticServerList, XonoticListBox)
5         METHOD(XonoticServerList, configureXonoticServerList, void(entity))
6         ATTRIB(XonoticServerList, rowsPerItem, float, 1)
7         METHOD(XonoticServerList, draw, void(entity))
8         METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
9         METHOD(XonoticServerList, doubleClickListBoxItem, void(entity, float, vector))
10         METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
11         METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
12         METHOD(XonoticServerList, toggleFavorite, void(entity, string))
13
14         ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
15
16         ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
17         ATTRIB(XonoticServerList, realUpperMargin, float, 0)
18         ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
19         ATTRIB(XonoticServerList, columnIconsSize, float, 0)
20         ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
21         ATTRIB(XonoticServerList, columnPingSize, float, 0)
22         ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
23         ATTRIB(XonoticServerList, columnNameSize, float, 0)
24         ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
25         ATTRIB(XonoticServerList, columnMapSize, float, 0)
26         ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
27         ATTRIB(XonoticServerList, columnTypeSize, float, 0)
28         ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
29         ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
30
31         ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
32         METHOD(XonoticServerList, setSelected, void(entity, float))
33         METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
34         ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
35         ATTRIB(XonoticServerList, filterShowFull, float, 1)
36         ATTRIB(XonoticServerList, filterString, string, string_null)
37         ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
38         ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
39         ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
40         ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
41         METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
42         ATTRIB(XonoticServerList, needsRefresh, float, 1)
43         METHOD(XonoticServerList, focusEnter, void(entity))
44         METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
45         ATTRIB(XonoticServerList, sortButton1, entity, NULL)
46         ATTRIB(XonoticServerList, sortButton2, entity, NULL)
47         ATTRIB(XonoticServerList, sortButton3, entity, NULL)
48         ATTRIB(XonoticServerList, sortButton4, entity, NULL)
49         ATTRIB(XonoticServerList, sortButton5, entity, NULL)
50         ATTRIB(XonoticServerList, connectButton, entity, NULL)
51         ATTRIB(XonoticServerList, infoButton, entity, NULL)
52         ATTRIB(XonoticServerList, currentSortOrder, float, 0)
53         ATTRIB(XonoticServerList, currentSortField, float, -1)
54
55         ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
56
57         ATTRIB(XonoticServerList, seenIPv4, float, 0)
58         ATTRIB(XonoticServerList, seenIPv6, float, 0)
59         ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
60
61         METHOD(XonoticServerList, getTotalHeight, float(entity))
62         METHOD(XonoticServerList, getItemAtPos, float(entity, float))
63         METHOD(XonoticServerList, getItemStart, float(entity, float))
64         METHOD(XonoticServerList, getItemHeight, float(entity, float))
65 ENDCLASS(XonoticServerList)
66 entity makeXonoticServerList();
67
68 #ifndef IMPLEMENTATION
69 float autocvar_menu_slist_categories;
70 float autocvar_menu_slist_categories_onlyifmultiple;
71 float autocvar_menu_slist_purethreshold;
72 float autocvar_menu_slist_modimpurity;
73 float autocvar_menu_slist_recommendations;
74 float autocvar_menu_slist_recommendations_maxping;
75 float autocvar_menu_slist_recommendations_minfreeslots;
76 float autocvar_menu_slist_recommendations_minhumans;
77 float autocvar_menu_slist_recommendations_purethreshold;
78
79 // server cache fields
80 #define SLIST_FIELDS \
81         SLIST_FIELD(CNAME,       "cname") \
82         SLIST_FIELD(PING,        "ping") \
83         SLIST_FIELD(GAME,        "game") \
84         SLIST_FIELD(MOD,         "mod") \
85         SLIST_FIELD(MAP,         "map") \
86         SLIST_FIELD(NAME,        "name") \
87         SLIST_FIELD(MAXPLAYERS,  "maxplayers") \
88         SLIST_FIELD(NUMPLAYERS,  "numplayers") \
89         SLIST_FIELD(NUMHUMANS,   "numhumans") \
90         SLIST_FIELD(NUMBOTS,     "numbots") \
91         SLIST_FIELD(PROTOCOL,    "protocol") \
92         SLIST_FIELD(FREESLOTS,   "freeslots") \
93         SLIST_FIELD(PLAYERS,     "players") \
94         SLIST_FIELD(QCSTATUS,    "qcstatus") \
95         SLIST_FIELD(CATEGORY,    "category") \
96         SLIST_FIELD(ISFAVORITE,  "isfavorite")
97
98 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
99 SLIST_FIELDS
100 #undef SLIST_FIELD
101
102 const float REFRESHSERVERLIST_RESORT = 0;    // sort the server list again to update for changes to e.g. favorite status, categories
103 const float REFRESHSERVERLIST_REFILTER = 1;  // ..., also update filter and sort criteria
104 const float REFRESHSERVERLIST_ASK = 2;       // ..., also suggest querying servers now
105 const float REFRESHSERVERLIST_RESET = 3;     // ..., also clear the list first
106
107 // function declarations
108 float IsServerInList(string list, string srv);
109 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
110 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
111 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
112
113 entity RetrieveCategoryEnt(float catnum);
114
115 float CheckCategoryOverride(float cat);
116 float CheckCategoryForEntry(float entry);
117 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
118
119 void RegisterSLCategories();
120
121 void ServerList_Connect_Click(entity btn, entity me);
122 void ServerList_Categories_Click(entity box, entity me);
123 void ServerList_ShowEmpty_Click(entity box, entity me);
124 void ServerList_ShowFull_Click(entity box, entity me);
125 void ServerList_Filter_Change(entity box, entity me);
126 void ServerList_Favorite_Click(entity btn, entity me);
127 void ServerList_Info_Click(entity btn, entity me);
128 void ServerList_Update_favoriteButton(entity btn, entity me);
129
130 // fields for category entities
131 const int MAX_CATEGORIES = 9;
132 const int CATEGORY_FIRST = 1;
133 entity categories[MAX_CATEGORIES];
134 int category_ent_count;
135 .string cat_name;
136 .string cat_string;
137 .string cat_enoverride_string;
138 .string cat_dioverride_string;
139 .float cat_enoverride;
140 .float cat_dioverride;
141
142 // fields for drawing categories
143 int category_name[MAX_CATEGORIES];
144 int category_item[MAX_CATEGORIES];
145 int category_draw_count;
146
147 #define SLIST_CATEGORIES \
148         SLIST_CATEGORY(CAT_FAVORITED,    "",            "",             ZCTX(_("SLCAT^Favorites"))) \
149         SLIST_CATEGORY(CAT_RECOMMENDED,  "",            "",             ZCTX(_("SLCAT^Recommended"))) \
150         SLIST_CATEGORY(CAT_NORMAL,       "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Normal Servers"))) \
151         SLIST_CATEGORY(CAT_SERVERS,      "CAT_NORMAL",  "CAT_SERVERS",  ZCTX(_("SLCAT^Servers"))) \
152         SLIST_CATEGORY(CAT_XPM,          "CAT_NORMAL",  "CAT_SERVERS",  ZCTX(_("SLCAT^Competitive Mode"))) \
153         SLIST_CATEGORY(CAT_MODIFIED,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Modified Servers"))) \
154         SLIST_CATEGORY(CAT_OVERKILL,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Overkill Mode"))) \
155         SLIST_CATEGORY(CAT_INSTAGIB,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^InstaGib Mode"))) \
156         SLIST_CATEGORY(CAT_DEFRAG,       "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Defrag Mode")))
157
158 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
159 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
160         int name; \
161         string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
162 SLIST_CATEGORIES
163 #undef SLIST_CATEGORY
164
165 #endif
166 #endif
167 #ifdef IMPLEMENTATION
168
169 void RegisterSLCategories()
170 {
171         entity cat;
172         #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
173                 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
174                 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
175                 cat = spawn(); \
176                 categories[name - 1] = cat; \
177                 cat.classname = "slist_category"; \
178                 cat.cat_name = strzone(#name); \
179                 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
180                 cat.cat_dioverride_string = strzone(dioverride); \
181                 cat.cat_string = strzone(str);
182         SLIST_CATEGORIES
183         #undef SLIST_CATEGORY
184
185         int i, x, catnum;
186         string s;
187
188         #define PROCESS_OVERRIDE(override_string,override_field) \
189                 for(i = 0; i < category_ent_count; ++i) \
190                 { \
191                         s = categories[i].override_string; \
192                         if((s != "") && (s != categories[i].cat_name)) \
193                         { \
194                                 catnum = 0; \
195                                 for(x = 0; x < category_ent_count; ++x) \
196                                 { if(categories[x].cat_name == s) { \
197                                         catnum = (x+1); \
198                                         break; \
199                                 } } \
200                                 if(catnum) \
201                                 { \
202                                         strunzone(categories[i].override_string); \
203                                         categories[i].override_field = catnum; \
204                                         continue; \
205                                 } \
206                                 else \
207                                 { \
208                                         printf( \
209                                                 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
210                                                 s, \
211                                                 categories[i].cat_name \
212                                         ); \
213                                 } \
214                         } \
215                         strunzone(categories[i].override_string); \
216                         categories[i].override_field = 0; \
217                 }
218         PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
219         PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
220         #undef PROCESS_OVERRIDE
221 }
222
223 // Supporting Functions
224 entity RetrieveCategoryEnt(int catnum)
225 {
226         if((catnum > 0) && (catnum <= category_ent_count))
227         {
228                 return categories[catnum - 1];
229         }
230         else
231         {
232                 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
233                 return world;
234         }
235 }
236
237 bool IsServerInList(string list, string srv)
238 {
239         string p;
240         int i, n;
241         if(srv == "")
242                 return false;
243         srv = netaddress_resolve(srv, 26000);
244         if(srv == "")
245                 return false;
246         p = crypto_getidfp(srv);
247         n = tokenize_console(list);
248         for(i = 0; i < n; ++i)
249         {
250                 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
251                 {
252                         if(p)
253                                 if(argv(i) == p)
254                                         return true;
255                 }
256                 else
257                 {
258                         if(srv == netaddress_resolve(argv(i), 26000))
259                                 return true;
260                 }
261         }
262         return false;
263 }
264
265 int CheckCategoryOverride(int cat)
266 {
267         entity catent = RetrieveCategoryEnt(cat);
268         if(catent)
269         {
270                 int override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
271                 if(override) { return override; }
272                 else { return cat; }
273         }
274         else
275         {
276                 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
277                 return cat;
278         }
279 }
280
281 int CheckCategoryForEntry(int entry)
282 {
283         string s, k, v, modtype = "";
284         int j, m, impure = 0, freeslots = 0, sflags = 0;
285         s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
286         m = tokenizebyseparator(s, ":");
287
288         for(j = 2; j < m; ++j)
289         {
290                 if(argv(j) == "") { break; }
291                 k = substring(argv(j), 0, 1);
292                 v = substring(argv(j), 1, -1);
293                 switch(k)
294                 {
295                         case "P": { impure = stof(v); break; }
296                         case "S": { freeslots = stof(v); break; }
297                         case "F": { sflags = stof(v); break; }
298                         case "M": { modtype = strtolower(v); break; }
299                 }
300         }
301
302         if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
303
304         // check if this server is favorited
305         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
306
307         // now check if it's recommended
308         if(autocvar_menu_slist_recommendations)
309         {
310                 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
311
312                 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
313                 else
314                 {
315                         float recommended = 0;
316                         if(autocvar_menu_slist_recommendations & 1)
317                         {
318                                 if(IsRecommended(cname)) { ++recommended; }
319                                 else { --recommended; }
320                         }
321                         if(autocvar_menu_slist_recommendations & 2)
322                         {
323                                 if(
324                                         ///// check for minimum free slots
325                                         (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
326
327                                         && // check for purity requirement
328                                         (
329                                                 (autocvar_menu_slist_recommendations_purethreshold < 0)
330                                                 ||
331                                                 (impure <= autocvar_menu_slist_recommendations_purethreshold)
332                                         )
333
334                                         && // check for minimum amount of humans
335                                         (
336                                                 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
337                                                 >=
338                                                 autocvar_menu_slist_recommendations_minhumans
339                                         )
340
341                                         && // check for maximum latency
342                                         (
343                                                 gethostcachenumber(SLIST_FIELD_PING, entry)
344                                                 <=
345                                                 autocvar_menu_slist_recommendations_maxping
346                                         )
347                                 )
348                                         { ++recommended; }
349                                 else
350                                         { --recommended; }
351                         }
352                         if(recommended > 0) { return CAT_RECOMMENDED; }
353                 }
354         }
355
356         // if not favorited or recommended, check modname
357         if(modtype != "xonotic")
358         {
359                 switch(modtype)
360                 {
361                         // old servers which don't report their mod name are considered modified now
362                         case "": { return CAT_MODIFIED; }
363
364                         case "xpm": { return CAT_XPM; }
365                         case "minstagib":
366                         case "instagib": { return CAT_INSTAGIB; }
367                         case "overkill": { return CAT_OVERKILL; }
368                         //case "nix": { return CAT_NIX; }
369                         //case "newtoys": { return CAT_NEWTOYS; }
370
371                         // "cts" is allowed as compat, xdf is replacement
372                         case "cts":
373                         case "xdf": { return CAT_DEFRAG; }
374
375                         default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
376                 }
377         }
378
379         // must be normal or impure server
380         return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
381 }
382
383 void XonoticServerList_toggleFavorite(entity me, string srv)
384 {
385         string s, s0, s1, s2, srv_resolved, p;
386         int i, n;
387         bool f = false;
388         srv_resolved = netaddress_resolve(srv, 26000);
389         p = crypto_getidfp(srv_resolved);
390         s = cvar_string("net_slist_favorites");
391         n = tokenize_console(s);
392         for(i = 0; i < n; ++i)
393         {
394                 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
395                 {
396                         if(p)
397                                 if(argv(i) != p)
398                                         continue;
399                 }
400                 else
401                 {
402                         if(srv_resolved != netaddress_resolve(argv(i), 26000))
403                                 continue;
404                 }
405                 s0 = s1 = s2 = "";
406                 if(i > 0)
407                         s0 = substring(s, 0, argv_end_index(i - 1));
408                 if(i < n-1)
409                         s2 = substring(s, argv_start_index(i + 1), -1);
410                 if(s0 != "" && s2 != "")
411                         s1 = " ";
412                 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
413                 s = cvar_string("net_slist_favorites");
414                 n = tokenize_console(s);
415                 f = true;
416                 --i;
417         }
418
419         if(!f)
420         {
421                 s1 = "";
422                 if(s != "")
423                         s1 = " ";
424                 if(p)
425                         cvar_set("net_slist_favorites", strcat(s, s1, p));
426                 else
427                         cvar_set("net_slist_favorites", strcat(s, s1, srv));
428         }
429
430         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
431 }
432
433 void ServerList_Update_favoriteButton(entity btn, entity me)
434 {
435         me.favoriteButton.setText(me.favoriteButton,
436                 (IsFavorite(me.ipAddressBox.text) ?
437                         _("Remove") : _("Favorite")
438                 )
439         );
440 }
441
442 entity makeXonoticServerList()
443 {
444         entity me;
445         me = NEW(XonoticServerList);
446         me.configureXonoticServerList(me);
447         return me;
448 }
449 void XonoticServerList_configureXonoticServerList(entity me)
450 {
451         me.configureXonoticListBox(me);
452
453         // update field ID's
454         #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
455         SLIST_FIELDS
456         #undef SLIST_FIELD
457
458         // clear list
459         me.nItems = 0;
460 }
461 void XonoticServerList_setSelected(entity me, int i)
462 {
463         //int save = me.selectedItem;
464         SUPER(XonoticServerList).setSelected(me, i);
465         /*
466         if(me.selectedItem == save)
467                 return;
468         */
469         if(me.nItems == 0)
470                 return;
471         if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
472                 return; // sorry, it would be wrong
473
474         if(me.selectedServer)
475                 strunzone(me.selectedServer);
476         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
477
478         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
479         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
480         me.ipAddressBoxFocused = -1;
481 }
482 void XonoticServerList_refreshServerList(entity me, int mode)
483 {
484         //print("refresh of type ", ftos(mode), "\n");
485
486         if(mode >= REFRESHSERVERLIST_REFILTER)
487         {
488                 float m;
489                 int i, n;
490                 int listflags = 0;
491                 string s, typestr, modstr;
492
493                 s = me.filterString;
494
495                 m = strstrofs(s, ":", 0);
496                 if(m >= 0)
497                 {
498                         typestr = substring(s, 0, m);
499                         s = substring(s, m + 1, strlen(s) - m - 1);
500                         while(substring(s, 0, 1) == " ")
501                                 s = substring(s, 1, strlen(s) - 1);
502                 }
503                 else
504                         typestr = "";
505
506                 modstr = cvar_string("menu_slist_modfilter");
507
508                 m = SLIST_MASK_AND - 1;
509                 resethostcachemasks();
510
511                 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
512                 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
513
514                 // show full button
515                 if(!me.filterShowFull)
516                 {
517                         sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
518                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
519                 }
520
521                 // show empty button
522                 if(!me.filterShowEmpty)
523                         sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
524
525                 // gametype filtering
526                 if(typestr != "")
527                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
528
529                 // mod filtering
530                 if(modstr != "")
531                 {
532                         if(substring(modstr, 0, 1) == "!")
533                                 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
534                         else
535                                 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
536                 }
537
538                 // server banning
539                 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
540                 for(i = 0; i < n; ++i)
541                         if(argv(i) != "")
542                                 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
543
544                 m = SLIST_MASK_OR - 1;
545                 if(s != "")
546                 {
547                         sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
548                         sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
549                         sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
550                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
551                 }
552
553                 // sorting flags
554                 //listflags |= SLSF_FAVORITES;
555                 listflags |= SLSF_CATEGORIES;
556                 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
557                 sethostcachesort(me.currentSortField, listflags);
558         }
559
560         resorthostcache();
561         if(mode >= REFRESHSERVERLIST_ASK)
562                 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
563 }
564 void XonoticServerList_focusEnter(entity me)
565 {
566         SUPER(XonoticServerList).focusEnter(me);
567         if(time < me.nextRefreshTime)
568         {
569                 //print("sorry, no refresh yet\n");
570                 return;
571         }
572         me.nextRefreshTime = time + 10;
573         me.refreshServerList(me, REFRESHSERVERLIST_ASK);
574 }
575
576 void XonoticServerList_draw(entity me)
577 {
578         int i;
579         bool found = false, owned;
580
581         if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
582         {
583                 if(!me.needsRefresh)
584                         me.needsRefresh = 2;
585                 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
586         }
587
588         if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
589         {
590                 if(!me.needsRefresh)
591                         me.needsRefresh = 3;
592                 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
593         }
594
595         if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
596         {
597                 if(!me.needsRefresh)
598                         me.needsRefresh = 3;
599                 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
600         }
601
602         if(me.currentSortField == -1)
603         {
604                 me.setSortOrder(me, SLIST_FIELD_PING, +1);
605                 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
606         }
607         else if(me.needsRefresh == 1)
608         {
609                 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
610         }
611         else if(me.needsRefresh == 2)
612         {
613                 me.needsRefresh = 0;
614                 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
615         }
616         else if(me.needsRefresh == 3)
617         {
618                 me.needsRefresh = 0;
619                 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
620         }
621
622         owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
623
624         for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
625         category_draw_count = 0;
626
627         if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
628         {
629                 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
630                 me.nItems = itemcount;
631
632                 //float visible = floor(me.scrollPos / me.itemHeight);
633                 // ^ unfortunately no such optimization can be made-- we must process through the
634                 // entire list, otherwise there is no way to know which item is first in its category.
635
636                 // binary search method suggested by div
637                 float x;
638                 float begin = 0;
639                 for(x = 1; x <= category_ent_count; ++x) {
640                         float first = begin;
641                         float last = (itemcount - 1);
642                         if (first > last) {
643                                 // List is empty.
644                                 break;
645                         }
646                         float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
647                         float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
648                         if (catf > x) {
649                                 // The first one is already > x.
650                                 // Therefore, category x does not exist.
651                                 // Higher numbered categories do exist though.
652                         } else if (catl < x) {
653                                 // The last one is < x.
654                                 // Thus this category - and any following -
655                                 // don't exist.
656                                 break;
657                         } else if (catf == x) {
658                                 // Starts at first. This breaks the loop
659                                 // invariant in the binary search and thus has
660                                 // to be handled separately.
661                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
662                                         error("Category mismatch I");
663                                 if(first > 0)
664                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
665                                                 error("Category mismatch II");
666                                 category_name[category_draw_count] = x;
667                                 category_item[category_draw_count] = first;
668                                 ++category_draw_count;
669                                 begin = first + 1;
670                         } else {
671                                 // At this point, catf <= x < catl, thus
672                                 // catf < catl, thus first < last.
673                                 // INVARIANTS:
674                                 // last - first >= 1
675                                 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
676                                 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
677                                 // catf < x
678                                 // catl >= x
679                                 while (last - first > 1) {
680                                         float middle = floor((first + last) / 2);
681                                         // By loop condition, middle != first && middle != last.
682                                         float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
683                                         if (cat >= x) {
684                                                 last = middle;
685                                                 catl = cat;
686                                         } else {
687                                                 first = middle;
688                                                 catf = cat;
689                                         }
690                                 }
691                                 if (catl == x) {
692                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
693                                                 error("Category mismatch III");
694                                         if(last > 0)
695                                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
696                                                         error("Category mismatch IV");
697                                         category_name[category_draw_count] = x;
698                                         category_item[category_draw_count] = last;
699                                         ++category_draw_count;
700                                         begin = last + 1; // already scanned through these, skip 'em
701                                 }
702                                 else
703                                         begin = last; // already scanned through these, skip 'em
704                         }
705                 }
706                 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
707                 {
708                         category_name[0] = -1;
709                         category_item[0] = -1;
710                         category_draw_count = 0;
711                         me.nItems = itemcount;
712                 }
713         }
714         else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
715
716         me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
717         me.infoButton.disabled = ((me.nItems == 0) || !owned);
718         me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
719
720         if(me.selectedServer)
721         {
722                 for(i = 0; i < me.nItems; ++i)
723                 {
724                         if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
725                         {
726                                 me.selectedItem = i;
727                                 found = true;
728                                 break;
729                         }
730                 }
731         }
732         if(!found)
733         {
734                 if(me.nItems > 0)
735                 {
736                         if(me.selectedItem >= me.nItems)
737                                 me.selectedItem = me.nItems - 1;
738                         if(me.selectedServer)
739                                 strunzone(me.selectedServer);
740                         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
741                 }
742         }
743
744         if(owned)
745         {
746                 if(me.selectedServer != me.ipAddressBox.text)
747                 {
748                         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
749                         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
750                         me.ipAddressBoxFocused = -1;
751                 }
752         }
753
754         if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
755         {
756                 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
757                         ServerList_Update_favoriteButton(NULL, me);
758                 me.ipAddressBoxFocused = me.ipAddressBox.focused;
759         }
760
761         SUPER(XonoticServerList).draw(me);
762 }
763 void ServerList_PingSort_Click(entity btn, entity me)
764 {
765         me.setSortOrder(me, SLIST_FIELD_PING, +1);
766 }
767 void ServerList_NameSort_Click(entity btn, entity me)
768 {
769         me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
770 }
771 void ServerList_MapSort_Click(entity btn, entity me)
772 {
773         me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
774 }
775 void ServerList_PlayerSort_Click(entity btn, entity me)
776 {
777         me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
778 }
779 void ServerList_TypeSort_Click(entity btn, entity me)
780 {
781         string s, t;
782         float i, m;
783         s = me.filterString;
784         m = strstrofs(s, ":", 0);
785         if(m >= 0)
786         {
787                 s = substring(s, 0, m);
788                 while(substring(s, m+1, 1) == " ") // skip spaces
789                         ++m;
790         }
791         else
792                 s = "";
793
794         for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
795         {
796                 t = MapInfo_Type_ToString(i);
797                 if(i > 1)
798                         if(t == "") // it repeats (default case)
799                         {
800                                 // no type was found
801                                 // choose the first one
802                                 s = MapInfo_Type_ToString(1);
803                                 break;
804                         }
805                 if(s == t)
806                 {
807                         // the type was found
808                         // choose the next one
809                         s = MapInfo_Type_ToString(i * 2);
810                         if(s == "")
811                                 s = MapInfo_Type_ToString(1);
812                         break;
813                 }
814         }
815
816         if(s != "")
817                 s = strcat(s, ":");
818         s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
819
820         me.controlledTextbox.setText(me.controlledTextbox, s);
821         me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
822         me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
823         //ServerList_Filter_Change(me.controlledTextbox, me);
824 }
825 void ServerList_Filter_Change(entity box, entity me)
826 {
827         if(me.filterString)
828                 strunzone(me.filterString);
829         if(box.text != "")
830                 me.filterString = strzone(box.text);
831         else
832                 me.filterString = string_null;
833         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
834
835         me.ipAddressBox.setText(me.ipAddressBox, "");
836         me.ipAddressBox.cursorPos = 0;
837         me.ipAddressBoxFocused = -1;
838 }
839 void ServerList_Categories_Click(entity box, entity me)
840 {
841         box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
842         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
843
844         me.ipAddressBox.setText(me.ipAddressBox, "");
845         me.ipAddressBox.cursorPos = 0;
846         me.ipAddressBoxFocused = -1;
847 }
848 void ServerList_ShowEmpty_Click(entity box, entity me)
849 {
850         box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
851         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
852
853         me.ipAddressBox.setText(me.ipAddressBox, "");
854         me.ipAddressBox.cursorPos = 0;
855         me.ipAddressBoxFocused = -1;
856 }
857 void ServerList_ShowFull_Click(entity box, entity me)
858 {
859         box.setChecked(box, me.filterShowFull = !me.filterShowFull);
860         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
861
862         me.ipAddressBox.setText(me.ipAddressBox, "");
863         me.ipAddressBox.cursorPos = 0;
864         me.ipAddressBoxFocused = -1;
865 }
866 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
867 {
868         if(me.currentSortField == fld)
869                 direction = -me.currentSortOrder;
870         me.currentSortOrder = direction;
871         me.currentSortField = fld;
872         me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
873         me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
874         me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
875         me.sortButton4.forcePressed = 0;
876         me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
877         me.selectedItem = 0;
878         if(me.selectedServer)
879                 strunzone(me.selectedServer);
880         me.selectedServer = string_null;
881         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
882 }
883 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
884 {
885         vector originInLBSpace, sizeInLBSpace;
886         originInLBSpace = eY * (-me.itemHeight);
887         sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
888
889         vector originInDialogSpace, sizeInDialogSpace;
890         originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
891         sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
892
893         btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
894         btn.Container_size_x   =                         sizeInDialogSpace.x * theSize;
895         btn.setText(btn, theTitle);
896         btn.onClick = theFunc;
897         btn.onClickEntity = me;
898         btn.resized = 1;
899 }
900 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
901 {
902         SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
903
904         me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
905         me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
906         me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
907
908         me.columnIconsOrigin = 0;
909         me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
910         me.columnPingSize = me.realFontSize.x * 3;
911         me.columnMapSize = me.realFontSize.x * 10;
912         me.columnTypeSize = me.realFontSize.x * 4;
913         me.columnPlayersSize = me.realFontSize.x * 5;
914         me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
915         me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
916         me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
917         me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
918         me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
919         me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
920
921         me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
922         me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
923         me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
924         me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
925         me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
926
927         int f = me.currentSortField;
928         if(f >= 0)
929         {
930                 me.currentSortField = -1;
931                 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
932         }
933 }
934 void ServerList_Connect_Click(entity btn, entity me)
935 {
936         localcmd(sprintf("connect %s\n",
937                 ((me.ipAddressBox.text != "") ?
938                         me.ipAddressBox.text : me.selectedServer
939                 )
940         ));
941 }
942 void ServerList_Favorite_Click(entity btn, entity me)
943 {
944         string ipstr;
945         ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
946         if(ipstr != "")
947         {
948                 m_play_click_sound(MENU_SOUND_SELECT);
949                 me.toggleFavorite(me, me.ipAddressBox.text);
950                 me.ipAddressBoxFocused = -1;
951         }
952 }
953 void ServerList_Info_Click(entity btn, entity me)
954 {
955         if (me.nItems != 0)
956                 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
957
958         vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
959         vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
960         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
961 }
962 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
963 {
964         ServerList_Connect_Click(NULL, me);
965 }
966 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected)
967 {
968         // layout: Ping, Server name, Map name, NP, TP, MP
969         float p;
970         int q;
971         bool isv4, isv6;
972         vector theColor;
973         float theAlpha;
974         bool pure = false;
975         int freeslots = -1, sflags = -1, j, m;
976         string s, typestr, versionstr, k, v, modname;
977
978         //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
979
980         vector oldscale = draw_scale;
981         vector oldshift = draw_shift;
982 #define SET_YRANGE(start,end) \
983         draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
984         draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
985
986         for (j = 0; j < category_draw_count; ++j) {
987                 // Matches exactly the headings with increased height.
988                 if (i == category_item[j])
989                         break;
990         }
991
992         if (j < category_draw_count)
993         {
994                 entity catent = RetrieveCategoryEnt(category_name[j]);
995                 if(catent)
996                 {
997                         SET_YRANGE(
998                                 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
999                                 me.categoriesHeight / (me.categoriesHeight + 1)
1000                         );
1001                         draw_Text(
1002                                 eY * me.realUpperMargin
1003                                 +
1004 #if 0
1005                                 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1006                                 catent.cat_string,
1007 #else
1008                                 eX * (me.columnNameOrigin),
1009                                 strcat(catent.cat_string, ":"),
1010 #endif
1011                                 me.realFontSize,
1012                                 SKINCOLOR_SERVERLIST_CATEGORY,
1013                                 SKINALPHA_SERVERLIST_CATEGORY,
1014                                 0
1015                         );
1016                         SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1017                 }
1018         }
1019
1020         if(isSelected)
1021                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1022
1023         s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1024         m = tokenizebyseparator(s, ":");
1025         typestr = "";
1026         if(m >= 2)
1027         {
1028                 typestr = argv(0);
1029                 versionstr = argv(1);
1030         }
1031         freeslots = -1;
1032         modname = "";
1033         for(j = 2; j < m; ++j)
1034         {
1035                 if(argv(j) == "")
1036                         break;
1037                 k = substring(argv(j), 0, 1);
1038                 v = substring(argv(j), 1, -1);
1039                 if(k == "P")
1040                         pure = stob(v);
1041                 else if(k == "S")
1042                         freeslots = stof(v);
1043                 else if(k == "F")
1044                         sflags = stoi(v);
1045                 else if(k == "M")
1046                         modname = v;
1047         }
1048
1049 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1050         if(modname == "")
1051                 modname = "Xonotic";
1052 #endif
1053
1054         /*
1055         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1056         s = gethostcachestring(SLIST_FIELD_MOD, i);
1057         if(s != "data")
1058                 if(modname == "Xonotic")
1059                         modname = s;
1060         */
1061
1062         // list the mods here on which the pure server check actually works
1063         if(modname != "Xonotic")
1064         if(modname != "InstaGib" || modname != "MinstaGib")
1065         if(modname != "CTS")
1066         if(modname != "NIX")
1067         if(modname != "NewToys")
1068                 pure = false;
1069
1070         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1071                 theAlpha = SKINALPHA_SERVERLIST_FULL;
1072         else if(freeslots == 0)
1073                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1074         else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1075                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1076         else
1077                 theAlpha = 1;
1078
1079         p = gethostcachenumber(SLIST_FIELD_PING, i);
1080         const int PING_LOW = 75;
1081         const int PING_MED = 200;
1082         const int PING_HIGH = 500;
1083         if(p < PING_LOW)
1084                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1085         else if(p < PING_MED)
1086                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1087         else if(p < PING_HIGH)
1088         {
1089                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1090                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1091         }
1092         else
1093         {
1094                 theColor = eX;
1095                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1096         }
1097
1098         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1099         {
1100                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1101                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1102         }
1103
1104         s = gethostcachestring(SLIST_FIELD_CNAME, i);
1105
1106         isv4 = isv6 = false;
1107         if(substring(s, 0, 1) == "[")
1108         {
1109                 isv6 = true;
1110                 me.seenIPv6 += 1;
1111         }
1112         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1113         {
1114                 isv4 = true;
1115                 me.seenIPv4 += 1;
1116         }
1117
1118         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1119         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1120         {
1121                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1122                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1123         }
1124
1125         if(q == 1)
1126         {
1127                 if(cvar("crypto_aeslevel") >= 2)
1128                         q |= 4;
1129         }
1130         if(q == 2)
1131         {
1132                 if(cvar("crypto_aeslevel") >= 1)
1133                         q |= 4;
1134         }
1135         if(q == 3)
1136                 q = 5;
1137         else if(q >= 3)
1138                 q -= 2;
1139         // possible status:
1140         // 0: crypto off
1141         // 1: AES possible
1142         // 2: AES recommended but not available
1143         // 3: AES possible and will be used
1144         // 4: AES recommended and will be used
1145         // 5: AES required
1146
1147         // --------------
1148         //  RENDER ICONS
1149         // --------------
1150         vector iconSize = '0 0 0';
1151         iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1152         iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1153
1154         vector iconPos = '0 0 0';
1155         iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1156         iconPos_y = (1 - iconSize.y) * 0.5;
1157
1158         string n;
1159
1160         if (!(me.seenIPv4 && me.seenIPv6))
1161         {
1162                 iconPos.x += iconSize.x * 0.5;
1163         }
1164         else if(me.seenIPv4 && me.seenIPv6)
1165         {
1166                 n = string_null;
1167                 if(isv6)
1168                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1169                 else if(isv4)
1170                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1171                 if(n)
1172                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1173                 iconPos.x += iconSize.x;
1174         }
1175
1176         if(q > 0)
1177         {
1178                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1179                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1180         }
1181         iconPos.x += iconSize.x;
1182
1183         if(modname == "Xonotic")
1184         {
1185                 if(pure == 0)
1186                 {
1187                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1188                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1189                 }
1190         }
1191         else
1192         {
1193                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1194                 if(draw_PictureSize(n) == '0 0 0')
1195                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1196                 if(pure == 0)
1197                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1198                 else
1199                         draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1200         }
1201         iconPos.x += iconSize.x;
1202
1203         if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1204         {
1205                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1206                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1207         }
1208         iconPos.x += iconSize.x;
1209
1210         // --------------
1211         //  RENDER TEXT
1212         // --------------
1213
1214         // ping
1215         s = ftos(p);
1216         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1217
1218         // server name
1219         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1220         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1221
1222         // server map
1223         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1224         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1225
1226         // server gametype
1227         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1228         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1229
1230         // server playercount
1231         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1232         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1233 }
1234
1235 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1236 {
1237         vector org, sz;
1238
1239         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1240         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1241
1242         if(scan == K_ENTER || scan == K_KP_ENTER)
1243         {
1244                 ServerList_Connect_Click(NULL, me);
1245                 return true;
1246         }
1247         else if(scan == K_MOUSE2 || scan == K_SPACE)
1248         {
1249                 if(me.nItems != 0)
1250                 {
1251                         m_play_click_sound(MENU_SOUND_OPEN);
1252                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1253                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1254                         return true;
1255                 }
1256                 return false;
1257         }
1258         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1259         {
1260                 if(me.nItems != 0)
1261                 {
1262                         me.toggleFavorite(me, me.selectedServer);
1263                         me.ipAddressBoxFocused = -1;
1264                         return true;
1265                 }
1266                 return false;
1267         }
1268         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1269                 return true;
1270         else if(!me.controlledTextbox)
1271                 return false;
1272         else
1273                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1274 }
1275
1276 float XonoticServerList_getTotalHeight(entity me)
1277 {
1278         float num_normal_rows = me.nItems;
1279         int num_headers = category_draw_count;
1280         return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1281 }
1282 int XonoticServerList_getItemAtPos(entity me, float pos)
1283 {
1284         pos = pos / me.itemHeight;
1285         int i;
1286         for (i = category_draw_count - 1; i >= 0; --i) {
1287                 int itemidx = category_item[i];
1288                 float itempos = i * me.categoriesHeight + category_item[i];
1289                 if (pos >= itempos + me.categoriesHeight + 1)
1290                         return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1291                 if (pos >= itempos)
1292                         return itemidx;
1293         }
1294         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1295         return floor(pos);
1296 }
1297 float XonoticServerList_getItemStart(entity me, int item)
1298 {
1299         int i;
1300         for (i = category_draw_count - 1; i >= 0; --i) {
1301                 int itemidx = category_item[i];
1302                 float itempos = i * me.categoriesHeight + category_item[i];
1303                 if (item >= itemidx + 1)
1304                         return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1305                 if (item >= itemidx)
1306                         return itempos * me.itemHeight;
1307         }
1308         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1309         return item * me.itemHeight;
1310 }
1311 float XonoticServerList_getItemHeight(entity me, int item)
1312 {
1313         int i;
1314         for (i = 0; i < category_draw_count; ++i) {
1315                 // Matches exactly the headings with increased height.
1316                 if (item == category_item[i])
1317                         return me.itemHeight * (me.categoriesHeight + 1);
1318         }
1319         return me.itemHeight;
1320 }
1321
1322 #endif