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