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