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