]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/xonotic/serverlist.c
use a proper binary search
[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                 // binary search method suggested by div
674                 float x;
675                 float begin = 0;
676                 for(x = 1; x <= category_ent_count; ++x) {
677                         float first = begin;
678                         float last = (itemcount - 1);
679                         if (first > last) {
680                                 // List is empty.
681                                 break;
682                         }
683                         float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
684                         float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
685                         if (catf > x) {
686                                 // The first one is already > x.
687                                 // Therefore, category x does not exist.
688                                 // Higher numbered categories do exist though.
689                         } else if (catl < x) {
690                                 // The last one is < x.
691                                 // Thus this category - and any following -
692                                 // don't exist.
693                                 break;
694                         } else if (catf == x) {
695                                 // Starts at first. This breaks the loop
696                                 // invariant in the binary search and thus has
697                                 // to be handled separately.
698                                 print(sprintf("hit: x='%d', dc='%d', first='%d', last='%d', catf='%d', catl='%d'.\n", x, category_draw_count, first, last, catf, catl));
699                                 category_name[category_draw_count] = x;
700                                 category_item[category_draw_count] = first;
701                                 ++category_draw_count;
702                                 ++me.nItems;
703                                 begin = first + 1;
704                         } else {
705                                 // At this point, catf <= x < catl, thus
706                                 // catf < catl, thus first < last.
707                                 // INVARIANTS:
708                                 // last - first >= 1
709                                 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
710                                 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
711                                 // catf < x
712                                 // catl >= x
713                                 while (last - first > 1) {
714                                         float middle = floor((first + last) / 2);
715                                         // By loop condition, middle != first && middle != last.
716                                         float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
717                                         if (cat >= x) {
718                                                 last = middle;
719                                                 catl = cat;
720                                         } else {
721                                                 first = middle;
722                                                 catf = cat;
723                                         }
724                                 }
725                                 if (catl == x) {
726                                         print(sprintf("hit: x='%d', dc='%d', first='%d', last='%d', catf='%d', catl='%d'.\n", x, category_draw_count, first, last, catf, catl));
727                                         category_name[category_draw_count] = x;
728                                         category_item[category_draw_count] = last;
729                                         ++category_draw_count;
730                                         ++me.nItems;
731                                 }
732                                 begin = last + 1; // already scanned through these, skip 'em
733                         }
734                 }
735
736                 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
737                 {
738                         category_name[0] = -1;
739                         category_item[0] = -1;
740                         category_draw_count = 0;
741                         me.nItems = itemcount;
742                 }
743         }
744         else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
745
746         me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
747         me.infoButton.disabled = ((me.nItems == 0) || !owned);
748         me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
749
750         found = 0;
751         if(me.selectedServer)
752         {
753                 for(i = 0; i < me.nItems; ++i)
754                 {
755                         num = CheckItemNumber(i);
756                         if(num >= 0)
757                         {
758                                 if(gethostcachestring(SLIST_FIELD_CNAME, num) == me.selectedServer)
759                                 {
760                                         if(i != me.selectedItem)
761                                         {
762                                                 me.lastClickedServer = -1;
763                                                 me.selectedItem = i;
764                                         }
765                                         found = 1;
766                                         break;
767                                 }
768                         }
769                 }
770         }
771         if(!found)
772         {
773                 if(me.nItems > 0)
774                 {
775                         if(me.selectedItem >= me.nItems) { me.selectedItem = me.nItems - 1; }
776                         if(me.selectedServer) { strunzone(me.selectedServer); }
777
778                         num = CheckItemNumber(me.selectedItem);
779                         if(num >= 0) { me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, num)); }
780                 }
781         }
782         
783         if(owned)
784         {
785                 if(me.selectedServer != me.ipAddressBox.text)
786                 {
787                         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
788                         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
789                         me.ipAddressBoxFocused = -1;
790                 }
791         }
792
793         if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
794         {
795                 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
796                         ServerList_Update_favoriteButton(NULL, me);
797                 me.ipAddressBoxFocused = me.ipAddressBox.focused;
798         }
799
800         SUPER(XonoticServerList).draw(me);
801 }
802 void ServerList_PingSort_Click(entity btn, entity me)
803 {
804         me.setSortOrder(me, SLIST_FIELD_PING, +1);
805 }
806 void ServerList_NameSort_Click(entity btn, entity me)
807 {
808         me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
809 }
810 void ServerList_MapSort_Click(entity btn, entity me)
811 {
812         me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
813 }
814 void ServerList_PlayerSort_Click(entity btn, entity me)
815 {
816         me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
817 }
818 void ServerList_TypeSort_Click(entity btn, entity me)
819 {
820         string s, t;
821         float i, m;
822         s = me.filterString;
823         m = strstrofs(s, ":", 0);
824         if(m >= 0)
825         {
826                 s = substring(s, 0, m);
827                 while(substring(s, m+1, 1) == " ") // skip spaces
828                         ++m;
829         }
830         else
831                 s = "";
832
833         for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
834         {
835                 t = MapInfo_Type_ToString(i);
836                 if(i > 1)
837                         if(t == "") // it repeats (default case)
838                         {
839                                 // no type was found
840                                 // choose the first one
841                                 s = MapInfo_Type_ToString(1);
842                                 break;
843                         }
844                 if(s == t)
845                 {
846                         // the type was found
847                         // choose the next one
848                         s = MapInfo_Type_ToString(i * 2);
849                         if(s == "")
850                                 s = MapInfo_Type_ToString(1);
851                         break;
852                 }
853         }
854
855         if(s != "")
856                 s = strcat(s, ":");
857         s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
858
859         me.controlledTextbox.setText(me.controlledTextbox, s);
860         me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
861         me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
862         //ServerList_Filter_Change(me.controlledTextbox, me);
863 }
864 void ServerList_Filter_Change(entity box, entity me)
865 {
866         if(me.filterString)
867                 strunzone(me.filterString);
868         if(box.text != "")
869                 me.filterString = strzone(box.text);
870         else
871                 me.filterString = string_null;
872         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
873
874         me.ipAddressBox.setText(me.ipAddressBox, "");
875         me.ipAddressBox.cursorPos = 0;
876         me.ipAddressBoxFocused = -1;
877 }
878 void ServerList_Categories_Click(entity box, entity me)
879 {
880         box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
881         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
882
883         me.ipAddressBox.setText(me.ipAddressBox, "");
884         me.ipAddressBox.cursorPos = 0;
885         me.ipAddressBoxFocused = -1;
886 }
887 void ServerList_ShowEmpty_Click(entity box, entity me)
888 {
889         box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
890         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
891
892         me.ipAddressBox.setText(me.ipAddressBox, "");
893         me.ipAddressBox.cursorPos = 0;
894         me.ipAddressBoxFocused = -1;
895 }
896 void ServerList_ShowFull_Click(entity box, entity me)
897 {
898         box.setChecked(box, me.filterShowFull = !me.filterShowFull);
899         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
900
901         me.ipAddressBox.setText(me.ipAddressBox, "");
902         me.ipAddressBox.cursorPos = 0;
903         me.ipAddressBoxFocused = -1;
904 }
905 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
906 {
907         if(me.currentSortField == fld)
908                 direction = -me.currentSortOrder;
909         me.currentSortOrder = direction;
910         me.currentSortField = fld;
911         me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
912         me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
913         me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
914         me.sortButton4.forcePressed = 0;
915         me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
916         me.selectedItem = 0;
917         if(me.selectedServer)
918                 strunzone(me.selectedServer);
919         me.selectedServer = string_null;
920         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
921 }
922 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
923 {
924         vector originInLBSpace, sizeInLBSpace;
925         originInLBSpace = eY * (-me.itemHeight);
926         sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
927
928         vector originInDialogSpace, sizeInDialogSpace;
929         originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
930         sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
931
932         btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
933         btn.Container_size_x   =                         sizeInDialogSpace_x * theSize;
934         btn.setText(btn, theTitle);
935         btn.onClick = theFunc;
936         btn.onClickEntity = me;
937         btn.resized = 1;
938 }
939 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
940 {
941         SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
942
943         me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
944         me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
945         me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
946
947         me.columnIconsOrigin = 0;
948         me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
949         me.columnPingSize = me.realFontSize_x * 3;
950         me.columnMapSize = me.realFontSize_x * 10;
951         me.columnTypeSize = me.realFontSize_x * 4;
952         me.columnPlayersSize = me.realFontSize_x * 5;
953         me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
954         me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
955         me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
956         me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
957         me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
958         me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
959
960         me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
961         me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
962         me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
963         me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
964         me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
965
966         float f;
967         f = me.currentSortField;
968         if(f >= 0)
969         {
970                 me.currentSortField = -1;
971                 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
972         }
973 }
974 void ServerList_Connect_Click(entity btn, entity me)
975 {
976         localcmd(sprintf("connect %s\n",
977                 ((me.ipAddressBox.text != "") ?
978                         me.ipAddressBox.text : me.selectedServer
979                 )
980         ));
981 }
982 void ServerList_Favorite_Click(entity btn, entity me)
983 {
984         string ipstr;
985         ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
986         if(ipstr != "")
987         {
988                 me.toggleFavorite(me, me.ipAddressBox.text);
989                 me.ipAddressBoxFocused = -1;
990         }
991 }
992 void ServerList_Info_Click(entity btn, entity me)
993 {
994         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, CheckItemNumber(me.selectedItem));
995         DialogOpenButton_Click(me, main.serverInfoDialog);
996 }
997 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
998 {
999         float num = CheckItemNumber(i);
1000         if(num >= 0)
1001         {
1002                 if(num == me.lastClickedServer)
1003                         if(time < me.lastClickedTime + 0.3)
1004                         {
1005                                 // DOUBLE CLICK!
1006                                 ServerList_Connect_Click(NULL, me);
1007                         }
1008                 me.lastClickedServer = num;
1009                 me.lastClickedTime = time;
1010         }
1011 }
1012 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
1013 {
1014         // layout: Ping, Server name, Map name, NP, TP, MP
1015         float p, q;
1016         float isv4, isv6;
1017         vector theColor;
1018         float theAlpha;
1019         float m, pure, freeslots, j, sflags;
1020         string s, typestr, versionstr, k, v, modname;
1021
1022         float item = CheckItemNumber(i);
1023         //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
1024         
1025         if(item < 0)
1026         {
1027                 entity catent = RetrieveCategoryEnt(-item);
1028                 if(catent)
1029                 {
1030                         draw_Text(
1031                                 eY * me.realUpperMargin
1032                                 +
1033                                 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1034                                 catent.cat_string,
1035                                 me.realFontSize,
1036                                 '1 1 1',
1037                                 SKINALPHA_TEXT,
1038                                 0
1039                         );
1040                         return;
1041                 }
1042         }
1043         
1044         if(isSelected)
1045                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1046
1047         s = gethostcachestring(SLIST_FIELD_QCSTATUS, item);
1048         m = tokenizebyseparator(s, ":");
1049         typestr = "";
1050         if(m >= 2)
1051         {
1052                 typestr = argv(0);
1053                 versionstr = argv(1);
1054         }
1055         freeslots = -1;
1056         sflags = -1;
1057         modname = "";
1058         pure = 0;
1059         for(j = 2; j < m; ++j)
1060         {
1061                 if(argv(j) == "")
1062                         break;
1063                 k = substring(argv(j), 0, 1);
1064                 v = substring(argv(j), 1, -1);
1065                 if(k == "P")
1066                         pure = stof(v);
1067                 else if(k == "S")
1068                         freeslots = stof(v);
1069                 else if(k == "F")
1070                         sflags = stof(v);
1071                 else if(k == "M")
1072                         modname = v;
1073         }
1074
1075 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1076         if(modname == "")
1077                 modname = "Xonotic";
1078 #endif
1079
1080         /*
1081         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1082         s = gethostcachestring(SLIST_FIELD_MOD, item);
1083         if(s != "data")
1084                 if(modname == "Xonotic")
1085                         modname = s;
1086         */
1087
1088         // list the mods here on which the pure server check actually works
1089         if(modname != "Xonotic")
1090         if(modname != "MinstaGib")
1091         if(modname != "CTS")
1092         if(modname != "NIX")
1093         if(modname != "NewToys")
1094                 pure = 0;
1095
1096         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, item) <= 0)
1097                 theAlpha = SKINALPHA_SERVERLIST_FULL;
1098         else if(freeslots == 0)
1099                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1100         else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item))
1101                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1102         else
1103                 theAlpha = 1;
1104
1105         p = gethostcachenumber(SLIST_FIELD_PING, item);
1106 #define PING_LOW 75
1107 #define PING_MED 200
1108 #define PING_HIGH 500
1109         if(p < PING_LOW)
1110                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1111         else if(p < PING_MED)
1112                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1113         else if(p < PING_HIGH)
1114         {
1115                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1116                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1117         }
1118         else
1119         {
1120                 theColor = eX;
1121                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1122         }
1123
1124         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, item))
1125         {
1126                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1127                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1128         }
1129
1130         s = gethostcachestring(SLIST_FIELD_CNAME, item);
1131
1132         isv4 = isv6 = 0;
1133         if(substring(s, 0, 1) == "[")
1134         {
1135                 isv6 = 1;
1136                 me.seenIPv6 += 1;
1137         }
1138         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1139         {
1140                 isv4 = 1;
1141                 me.seenIPv4 += 1;
1142         }
1143
1144         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1145         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1146         {
1147                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1148                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1149         }
1150
1151         if(q == 1)
1152         {
1153                 if(cvar("crypto_aeslevel") >= 2)
1154                         q |= 4;
1155         }
1156         if(q == 2)
1157         {
1158                 if(cvar("crypto_aeslevel") >= 1)
1159                         q |= 4;
1160         }
1161         if(q == 3)
1162                 q = 5;
1163         else if(q >= 3)
1164                 q -= 2;
1165         // possible status:
1166         // 0: crypto off
1167         // 1: AES possible
1168         // 2: AES recommended but not available
1169         // 3: AES possible and will be used
1170         // 4: AES recommended and will be used
1171         // 5: AES required
1172
1173         // --------------
1174         //  RENDER ICONS
1175         // --------------
1176         vector iconSize = '0 0 0';
1177         iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1178         iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1179
1180         vector iconPos = '0 0 0';
1181         iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1182         iconPos_y = (1 - iconSize_y) * 0.5;
1183
1184         string n;
1185
1186         if not(me.seenIPv4 && me.seenIPv6)
1187         {
1188                 iconPos_x += iconSize_x * 0.5;
1189         }
1190         else if(me.seenIPv4 && me.seenIPv6)
1191         {
1192                 n = string_null;
1193                 if(isv6)
1194                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1195                 else if(isv4)
1196                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1197                 if(n)
1198                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1199                 iconPos_x += iconSize_x;
1200         }
1201
1202         if(q > 0)
1203         {
1204                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1205                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1206         }
1207         iconPos_x += iconSize_x;
1208
1209         if(modname == "Xonotic")
1210         {
1211                 if(pure == 0)
1212                 {
1213                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1214                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1215                 }
1216         }
1217         else
1218         {
1219                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1220                 if(draw_PictureSize(n) == '0 0 0')
1221                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1222                 if(pure == 0)
1223                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1224                 else
1225                         draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1226         }
1227         iconPos_x += iconSize_x;
1228
1229         if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1230         {
1231                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1232                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1233         }
1234         iconPos_x += iconSize_x;
1235         
1236         // --------------
1237         //  RENDER TEXT
1238         // --------------
1239         
1240         // ping
1241         s = ftos(p);
1242         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1243
1244         // server name
1245         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, item), me.columnNameSize, 0, me.realFontSize);
1246         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1247
1248         // server map
1249         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, item), me.columnMapSize, 0, me.realFontSize);
1250         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1251
1252         // server gametype
1253         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1254         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1255
1256         // server playercount
1257         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, item)));
1258         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1259 }
1260
1261 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1262 {
1263         float i = CheckItemNumber(me.selectedItem);
1264         vector org, sz;
1265
1266         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1267         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1268
1269         me.lastBumpSelectTime = 0;
1270
1271         if(scan == K_ENTER || scan == K_KP_ENTER)
1272         {
1273                 ServerList_Connect_Click(NULL, me);
1274                 return 1;
1275         }
1276         else if(scan == K_MOUSE2 || scan == K_SPACE)
1277         {
1278                 if((me.nItems != 0) && (i >= 0))
1279                 {
1280                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1281                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1282                         return 1;
1283                 }
1284                 return 0;
1285         }
1286         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1287         {
1288                 if((me.nItems != 0) && (i >= 0))
1289                 {
1290                         me.toggleFavorite(me, me.selectedServer);
1291                         me.ipAddressBoxFocused = -1;
1292                         return 1;
1293                 }
1294                 return 0;
1295         }
1296         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1297                 return 1;
1298         else if(!me.controlledTextbox)
1299                 return 0;
1300         else
1301                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1302 }
1303 #endif