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