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