Listbox / Picker: Implement item fading in a different way so that it gets influenced...
[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, int, vector, bool, bool))
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 int MAX_CATEGORIES = 9;
130 const int CATEGORY_FIRST = 1;
131 entity categories[MAX_CATEGORIES];
132 int 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 int category_name[MAX_CATEGORIES];
142 int category_item[MAX_CATEGORIES];
143 int 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         int 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         int 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(int 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 bool IsServerInList(string list, string srv)
236 {
237         string p;
238         int 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 int CheckCategoryOverride(int cat)
264 {
265         entity catent = RetrieveCategoryEnt(cat);
266         if(catent)
267         {
268                 int 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 int CheckCategoryForEntry(int entry)
280 {
281         string s, k, v, modtype = "";
282         int 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         int i, n;
385         bool f = false;
386         srv_resolved = netaddress_resolve(srv, 26000);
387         p = crypto_getidfp(srv_resolved);
388         s = cvar_string("net_slist_favorites");
389         n = tokenize_console(s);
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 = true;
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, int i)
460 {
461         //int save = me.selectedItem;
462         SUPER(XonoticServerList).setSelected(me, i);
463         /*
464         if(me.selectedItem == save)
465                 return;
466         */
467         if(me.nItems == 0)
468                 return;
469         if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
470                 return; // sorry, it would be wrong
471
472         if(me.selectedServer)
473                 strunzone(me.selectedServer);
474         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
475
476         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
477         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
478         me.ipAddressBoxFocused = -1;
479 }
480 void XonoticServerList_refreshServerList(entity me, int mode)
481 {
482         //print("refresh of type ", ftos(mode), "\n");
483
484         if(mode >= REFRESHSERVERLIST_REFILTER)
485         {
486                 float m;
487                 int i, n;
488                 int 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         SUPER(XonoticServerList).focusEnter(me);
565         if(time < me.nextRefreshTime)
566         {
567                 //print("sorry, no refresh yet\n");
568                 return;
569         }
570         me.nextRefreshTime = time + 10;
571         me.refreshServerList(me, REFRESHSERVERLIST_ASK);
572 }
573
574 void XonoticServerList_draw(entity me)
575 {
576         int i;
577         bool found = false, owned;
578
579         if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
580         {
581                 if(!me.needsRefresh)
582                         me.needsRefresh = 2;
583                 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
584         }
585
586         if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
587         {
588                 if(!me.needsRefresh)
589                         me.needsRefresh = 3;
590                 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
591         }
592
593         if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
594         {
595                 if(!me.needsRefresh)
596                         me.needsRefresh = 3;
597                 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
598         }
599
600         if(me.currentSortField == -1)
601         {
602                 me.setSortOrder(me, SLIST_FIELD_PING, +1);
603                 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
604         }
605         else if(me.needsRefresh == 1)
606         {
607                 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
608         }
609         else if(me.needsRefresh == 2)
610         {
611                 me.needsRefresh = 0;
612                 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
613         }
614         else if(me.needsRefresh == 3)
615         {
616                 me.needsRefresh = 0;
617                 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
618         }
619
620         owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
621
622         for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
623         category_draw_count = 0;
624
625         if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
626         {
627                 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
628                 me.nItems = itemcount;
629
630                 //float visible = floor(me.scrollPos / me.itemHeight);
631                 // ^ unfortunately no such optimization can be made-- we must process through the
632                 // entire list, otherwise there is no way to know which item is first in its category.
633
634                 // binary search method suggested by div
635                 float x;
636                 float begin = 0;
637                 for(x = 1; x <= category_ent_count; ++x) {
638                         float first = begin;
639                         float last = (itemcount - 1);
640                         if (first > last) {
641                                 // List is empty.
642                                 break;
643                         }
644                         float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
645                         float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
646                         if (catf > x) {
647                                 // The first one is already > x.
648                                 // Therefore, category x does not exist.
649                                 // Higher numbered categories do exist though.
650                         } else if (catl < x) {
651                                 // The last one is < x.
652                                 // Thus this category - and any following -
653                                 // don't exist.
654                                 break;
655                         } else if (catf == x) {
656                                 // Starts at first. This breaks the loop
657                                 // invariant in the binary search and thus has
658                                 // to be handled separately.
659                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
660                                         error("Category mismatch I");
661                                 if(first > 0)
662                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
663                                                 error("Category mismatch II");
664                                 category_name[category_draw_count] = x;
665                                 category_item[category_draw_count] = first;
666                                 ++category_draw_count;
667                                 begin = first + 1;
668                         } else {
669                                 // At this point, catf <= x < catl, thus
670                                 // catf < catl, thus first < last.
671                                 // INVARIANTS:
672                                 // last - first >= 1
673                                 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
674                                 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
675                                 // catf < x
676                                 // catl >= x
677                                 while (last - first > 1) {
678                                         float middle = floor((first + last) / 2);
679                                         // By loop condition, middle != first && middle != last.
680                                         float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
681                                         if (cat >= x) {
682                                                 last = middle;
683                                                 catl = cat;
684                                         } else {
685                                                 first = middle;
686                                                 catf = cat;
687                                         }
688                                 }
689                                 if (catl == x) {
690                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
691                                                 error("Category mismatch III");
692                                         if(last > 0)
693                                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
694                                                         error("Category mismatch IV");
695                                         category_name[category_draw_count] = x;
696                                         category_item[category_draw_count] = last;
697                                         ++category_draw_count;
698                                         begin = last + 1; // already scanned through these, skip 'em
699                                 }
700                                 else
701                                         begin = last; // already scanned through these, skip 'em
702                         }
703                 }
704                 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
705                 {
706                         category_name[0] = -1;
707                         category_item[0] = -1;
708                         category_draw_count = 0;
709                         me.nItems = itemcount;
710                 }
711         }
712         else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
713
714         me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
715         me.infoButton.disabled = ((me.nItems == 0) || !owned);
716         me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
717
718         if(me.selectedServer)
719         {
720                 for(i = 0; i < me.nItems; ++i)
721                 {
722                         if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
723                         {
724                                 me.selectedItem = i;
725                                 found = true;
726                                 break;
727                         }
728                 }
729         }
730         if(!found)
731         {
732                 if(me.nItems > 0)
733                 {
734                         if(me.selectedItem >= me.nItems)
735                                 me.selectedItem = me.nItems - 1;
736                         if(me.selectedServer)
737                                 strunzone(me.selectedServer);
738                         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
739                 }
740         }
741
742         if(owned)
743         {
744                 if(me.selectedServer != me.ipAddressBox.text)
745                 {
746                         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
747                         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
748                         me.ipAddressBoxFocused = -1;
749                 }
750         }
751
752         if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
753         {
754                 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
755                         ServerList_Update_favoriteButton(NULL, me);
756                 me.ipAddressBoxFocused = me.ipAddressBox.focused;
757         }
758
759         SUPER(XonoticServerList).draw(me);
760 }
761 void ServerList_PingSort_Click(entity btn, entity me)
762 {
763         me.setSortOrder(me, SLIST_FIELD_PING, +1);
764 }
765 void ServerList_NameSort_Click(entity btn, entity me)
766 {
767         me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
768 }
769 void ServerList_MapSort_Click(entity btn, entity me)
770 {
771         me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
772 }
773 void ServerList_PlayerSort_Click(entity btn, entity me)
774 {
775         me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
776 }
777 void ServerList_TypeSort_Click(entity btn, entity me)
778 {
779         string s, t;
780         float i, m;
781         s = me.filterString;
782         m = strstrofs(s, ":", 0);
783         if(m >= 0)
784         {
785                 s = substring(s, 0, m);
786                 while(substring(s, m+1, 1) == " ") // skip spaces
787                         ++m;
788         }
789         else
790                 s = "";
791
792         for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
793         {
794                 t = MapInfo_Type_ToString(i);
795                 if(i > 1)
796                         if(t == "") // it repeats (default case)
797                         {
798                                 // no type was found
799                                 // choose the first one
800                                 s = MapInfo_Type_ToString(1);
801                                 break;
802                         }
803                 if(s == t)
804                 {
805                         // the type was found
806                         // choose the next one
807                         s = MapInfo_Type_ToString(i * 2);
808                         if(s == "")
809                                 s = MapInfo_Type_ToString(1);
810                         break;
811                 }
812         }
813
814         if(s != "")
815                 s = strcat(s, ":");
816         s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
817
818         me.controlledTextbox.setText(me.controlledTextbox, s);
819         me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
820         me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
821         //ServerList_Filter_Change(me.controlledTextbox, me);
822 }
823 void ServerList_Filter_Change(entity box, entity me)
824 {
825         if(me.filterString)
826                 strunzone(me.filterString);
827         if(box.text != "")
828                 me.filterString = strzone(box.text);
829         else
830                 me.filterString = string_null;
831         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
832
833         me.ipAddressBox.setText(me.ipAddressBox, "");
834         me.ipAddressBox.cursorPos = 0;
835         me.ipAddressBoxFocused = -1;
836 }
837 void ServerList_Categories_Click(entity box, entity me)
838 {
839         box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
840         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
841
842         me.ipAddressBox.setText(me.ipAddressBox, "");
843         me.ipAddressBox.cursorPos = 0;
844         me.ipAddressBoxFocused = -1;
845 }
846 void ServerList_ShowEmpty_Click(entity box, entity me)
847 {
848         box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
849         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
850
851         me.ipAddressBox.setText(me.ipAddressBox, "");
852         me.ipAddressBox.cursorPos = 0;
853         me.ipAddressBoxFocused = -1;
854 }
855 void ServerList_ShowFull_Click(entity box, entity me)
856 {
857         box.setChecked(box, me.filterShowFull = !me.filterShowFull);
858         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
859
860         me.ipAddressBox.setText(me.ipAddressBox, "");
861         me.ipAddressBox.cursorPos = 0;
862         me.ipAddressBoxFocused = -1;
863 }
864 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
865 {
866         if(me.currentSortField == fld)
867                 direction = -me.currentSortOrder;
868         me.currentSortOrder = direction;
869         me.currentSortField = fld;
870         me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
871         me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
872         me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
873         me.sortButton4.forcePressed = 0;
874         me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
875         me.selectedItem = 0;
876         if(me.selectedServer)
877                 strunzone(me.selectedServer);
878         me.selectedServer = string_null;
879         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
880 }
881 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
882 {
883         vector originInLBSpace, sizeInLBSpace;
884         originInLBSpace = eY * (-me.itemHeight);
885         sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
886
887         vector originInDialogSpace, sizeInDialogSpace;
888         originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
889         sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
890
891         btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
892         btn.Container_size_x   =                         sizeInDialogSpace.x * theSize;
893         btn.setText(btn, theTitle);
894         btn.onClick = theFunc;
895         btn.onClickEntity = me;
896         btn.resized = 1;
897 }
898 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
899 {
900         SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
901
902         me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
903         me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
904         me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
905
906         me.columnIconsOrigin = 0;
907         me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
908         me.columnPingSize = me.realFontSize.x * 3;
909         me.columnMapSize = me.realFontSize.x * 10;
910         me.columnTypeSize = me.realFontSize.x * 4;
911         me.columnPlayersSize = me.realFontSize.x * 5;
912         me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
913         me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
914         me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
915         me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
916         me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
917         me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
918
919         me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
920         me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
921         me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
922         me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
923         me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
924
925         int 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                 m_play_click_sound(MENU_SOUND_SELECT);
947                 me.toggleFavorite(me, me.ipAddressBox.text);
948                 me.ipAddressBoxFocused = -1;
949         }
950 }
951 void ServerList_Info_Click(entity btn, entity me)
952 {
953         if (me.nItems != 0)
954                 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
955
956         vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
957         vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
958         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
959 }
960 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
961 {
962         ServerList_Connect_Click(NULL, me);
963 }
964 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
965 {
966         // layout: Ping, Server name, Map name, NP, TP, MP
967         float p;
968         int q;
969         bool isv4, isv6;
970         vector theColor;
971         float theAlpha;
972         bool pure = false;
973         int freeslots = -1, sflags = -1, j, m;
974         string s, typestr, versionstr, k, v, modname;
975
976         //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
977
978         vector oldscale = draw_scale;
979         vector oldshift = draw_shift;
980 #define SET_YRANGE(start,end) \
981         draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
982         draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
983
984         for (j = 0; j < category_draw_count; ++j) {
985                 // Matches exactly the headings with increased height.
986                 if (i == category_item[j])
987                         break;
988         }
989
990         if (j < category_draw_count)
991         {
992                 entity catent = RetrieveCategoryEnt(category_name[j]);
993                 if(catent)
994                 {
995                         SET_YRANGE(
996                                 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
997                                 me.categoriesHeight / (me.categoriesHeight + 1)
998                         );
999                         draw_Text(
1000                                 eY * me.realUpperMargin
1001                                 +
1002 #if 0
1003                                 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1004                                 catent.cat_string,
1005 #else
1006                                 eX * (me.columnNameOrigin),
1007                                 strcat(catent.cat_string, ":"),
1008 #endif
1009                                 me.realFontSize,
1010                                 SKINCOLOR_SERVERLIST_CATEGORY,
1011                                 SKINALPHA_SERVERLIST_CATEGORY,
1012                                 0
1013                         );
1014                         SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1015                 }
1016         }
1017
1018         if(isSelected)
1019                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1020         else if(isFocused)
1021         {
1022                 me.focusedItemAlpha = getFadedAlpha(me.focusedItemAlpha, SKINALPHA_LISTBOX_FOCUSED, SKINFADEALPHA_LISTBOX_FOCUSED);
1023                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_FOCUSED, me.focusedItemAlpha);
1024         }
1025
1026         s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1027         m = tokenizebyseparator(s, ":");
1028         typestr = "";
1029         if(m >= 2)
1030         {
1031                 typestr = argv(0);
1032                 versionstr = argv(1);
1033         }
1034         freeslots = -1;
1035         modname = "";
1036         for(j = 2; j < m; ++j)
1037         {
1038                 if(argv(j) == "")
1039                         break;
1040                 k = substring(argv(j), 0, 1);
1041                 v = substring(argv(j), 1, -1);
1042                 if(k == "P")
1043                         pure = stob(v);
1044                 else if(k == "S")
1045                         freeslots = stof(v);
1046                 else if(k == "F")
1047                         sflags = stoi(v);
1048                 else if(k == "M")
1049                         modname = v;
1050         }
1051
1052 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1053         if(modname == "")
1054                 modname = "Xonotic";
1055 #endif
1056
1057         /*
1058         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1059         s = gethostcachestring(SLIST_FIELD_MOD, i);
1060         if(s != "data")
1061                 if(modname == "Xonotic")
1062                         modname = s;
1063         */
1064
1065         // list the mods here on which the pure server check actually works
1066         if(modname != "Xonotic")
1067         if(modname != "InstaGib" || modname != "MinstaGib")
1068         if(modname != "CTS")
1069         if(modname != "NIX")
1070         if(modname != "NewToys")
1071                 pure = false;
1072
1073         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1074                 theAlpha = SKINALPHA_SERVERLIST_FULL;
1075         else if(freeslots == 0)
1076                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1077         else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1078                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1079         else
1080                 theAlpha = 1;
1081
1082         p = gethostcachenumber(SLIST_FIELD_PING, i);
1083         const int PING_LOW = 75;
1084         const int PING_MED = 200;
1085         const int PING_HIGH = 500;
1086         if(p < PING_LOW)
1087                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1088         else if(p < PING_MED)
1089                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1090         else if(p < PING_HIGH)
1091         {
1092                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1093                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1094         }
1095         else
1096         {
1097                 theColor = eX;
1098                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1099         }
1100
1101         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1102         {
1103                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1104                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1105         }
1106
1107         s = gethostcachestring(SLIST_FIELD_CNAME, i);
1108
1109         isv4 = isv6 = false;
1110         if(substring(s, 0, 1) == "[")
1111         {
1112                 isv6 = true;
1113                 me.seenIPv6 += 1;
1114         }
1115         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1116         {
1117                 isv4 = true;
1118                 me.seenIPv4 += 1;
1119         }
1120
1121         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1122         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1123         {
1124                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1125                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1126         }
1127
1128         if(q == 1)
1129         {
1130                 if(cvar("crypto_aeslevel") >= 2)
1131                         q |= 4;
1132         }
1133         if(q == 2)
1134         {
1135                 if(cvar("crypto_aeslevel") >= 1)
1136                         q |= 4;
1137         }
1138         if(q == 3)
1139                 q = 5;
1140         else if(q >= 3)
1141                 q -= 2;
1142         // possible status:
1143         // 0: crypto off
1144         // 1: AES possible
1145         // 2: AES recommended but not available
1146         // 3: AES possible and will be used
1147         // 4: AES recommended and will be used
1148         // 5: AES required
1149
1150         // --------------
1151         //  RENDER ICONS
1152         // --------------
1153         vector iconSize = '0 0 0';
1154         iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1155         iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1156
1157         vector iconPos = '0 0 0';
1158         iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1159         iconPos_y = (1 - iconSize.y) * 0.5;
1160
1161         string n;
1162
1163         if (!(me.seenIPv4 && me.seenIPv6))
1164         {
1165                 iconPos.x += iconSize.x * 0.5;
1166         }
1167         else if(me.seenIPv4 && me.seenIPv6)
1168         {
1169                 n = string_null;
1170                 if(isv6)
1171                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1172                 else if(isv4)
1173                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1174                 if(n)
1175                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1176                 iconPos.x += iconSize.x;
1177         }
1178
1179         if(q > 0)
1180         {
1181                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1182                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1183         }
1184         iconPos.x += iconSize.x;
1185
1186         if(modname == "Xonotic")
1187         {
1188                 if(pure == 0)
1189                 {
1190                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1191                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1192                 }
1193         }
1194         else
1195         {
1196                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1197                 if(draw_PictureSize(n) == '0 0 0')
1198                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1199                 if(pure == 0)
1200                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1201                 else
1202                         draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1203         }
1204         iconPos.x += iconSize.x;
1205
1206         if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1207         {
1208                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1209                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1210         }
1211         iconPos.x += iconSize.x;
1212
1213         // --------------
1214         //  RENDER TEXT
1215         // --------------
1216
1217         // ping
1218         s = ftos(p);
1219         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1220
1221         // server name
1222         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1223         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1224
1225         // server map
1226         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1227         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1228
1229         // server gametype
1230         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1231         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1232
1233         // server playercount
1234         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1235         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1236 }
1237
1238 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1239 {
1240         vector org, sz;
1241
1242         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1243         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1244
1245         if(scan == K_ENTER || scan == K_KP_ENTER)
1246         {
1247                 ServerList_Connect_Click(NULL, me);
1248                 return true;
1249         }
1250         else if(scan == K_MOUSE2 || scan == K_SPACE)
1251         {
1252                 if(me.nItems != 0)
1253                 {
1254                         m_play_click_sound(MENU_SOUND_OPEN);
1255                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1256                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1257                         return true;
1258                 }
1259                 return false;
1260         }
1261         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1262         {
1263                 if(me.nItems != 0)
1264                 {
1265                         me.toggleFavorite(me, me.selectedServer);
1266                         me.ipAddressBoxFocused = -1;
1267                         return true;
1268                 }
1269                 return false;
1270         }
1271         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1272                 return true;
1273         else if(!me.controlledTextbox)
1274                 return false;
1275         else
1276                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1277 }
1278
1279 float XonoticServerList_getTotalHeight(entity me)
1280 {
1281         float num_normal_rows = me.nItems;
1282         int num_headers = category_draw_count;
1283         return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1284 }
1285 int XonoticServerList_getItemAtPos(entity me, float pos)
1286 {
1287         pos = pos / me.itemHeight;
1288         int i;
1289         for (i = category_draw_count - 1; i >= 0; --i) {
1290                 int itemidx = category_item[i];
1291                 float itempos = i * me.categoriesHeight + category_item[i];
1292                 if (pos >= itempos + me.categoriesHeight + 1)
1293                         return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1294                 if (pos >= itempos)
1295                         return itemidx;
1296         }
1297         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1298         return floor(pos);
1299 }
1300 float XonoticServerList_getItemStart(entity me, int item)
1301 {
1302         int i;
1303         for (i = category_draw_count - 1; i >= 0; --i) {
1304                 int itemidx = category_item[i];
1305                 float itempos = i * me.categoriesHeight + category_item[i];
1306                 if (item >= itemidx + 1)
1307                         return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1308                 if (item >= itemidx)
1309                         return itempos * me.itemHeight;
1310         }
1311         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1312         return item * me.itemHeight;
1313 }
1314 float XonoticServerList_getItemHeight(entity me, int item)
1315 {
1316         int i;
1317         for (i = 0; i < category_draw_count; ++i) {
1318                 // Matches exactly the headings with increased height.
1319                 if (item == category_item[i])
1320                         return me.itemHeight * (me.categoriesHeight + 1);
1321         }
1322         return me.itemHeight;
1323 }
1324
1325 #endif