]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/xonotic/serverlist.c
Merge branch 'master' into samual/combined_updates
[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_MINSTAGIB,    "",            "CAT_SERVERS",  ZCTX(_("SLCAT^MinstaGib 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": { return CAT_MINSTAGIB; }
366                         case "overkill": { return CAT_OVERKILL; }
367                         //case "nix": { return CAT_NIX; }
368                         //case "newtoys": { return CAT_NEWTOYS; }
369
370                         // "cts" is allowed as compat, xdf is replacement
371                         case "cts": 
372                         case "xdf": { return CAT_DEFRAG; }
373                         
374                         default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
375                 }
376         }
377
378         // must be normal or impure server
379         return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
380 }
381
382 void XonoticServerList_toggleFavorite(entity me, string srv)
383 {
384         string s, s0, s1, s2, srv_resolved, p;
385         float i, n, f;
386         srv_resolved = netaddress_resolve(srv, 26000);
387         p = crypto_getidfp(srv_resolved);
388         s = cvar_string("net_slist_favorites");
389         n = tokenize_console(s);
390         f = 0;
391         for(i = 0; i < n; ++i)
392         {
393                 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
394                 {
395                         if(p)
396                                 if(argv(i) != p)
397                                         continue;
398                 }
399                 else
400                 {
401                         if(srv_resolved != netaddress_resolve(argv(i), 26000))
402                                 continue;
403                 }
404                 s0 = s1 = s2 = "";
405                 if(i > 0)
406                         s0 = substring(s, 0, argv_end_index(i - 1));
407                 if(i < n-1)
408                         s2 = substring(s, argv_start_index(i + 1), -1);
409                 if(s0 != "" && s2 != "")
410                         s1 = " ";
411                 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
412                 s = cvar_string("net_slist_favorites");
413                 n = tokenize_console(s);
414                 f = 1;
415                 --i;
416         }
417
418         if(!f)
419         {
420                 s1 = "";
421                 if(s != "")
422                         s1 = " ";
423                 if(p)
424                         cvar_set("net_slist_favorites", strcat(s, s1, p));
425                 else
426                         cvar_set("net_slist_favorites", strcat(s, s1, srv));
427         }
428
429         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
430 }
431
432 void ServerList_Update_favoriteButton(entity btn, entity me)
433 {
434         me.favoriteButton.setText(me.favoriteButton,
435                 (IsFavorite(me.ipAddressBox.text) ?
436                         _("Remove") : _("Favorite")
437                 )
438         );
439 }
440
441 entity makeXonoticServerList()
442 {
443         entity me;
444         me = spawnXonoticServerList();
445         me.configureXonoticServerList(me);
446         return me;
447 }
448 void XonoticServerList_configureXonoticServerList(entity me)
449 {
450         me.configureXonoticListBox(me);
451
452         // update field ID's
453         #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
454         SLIST_FIELDS
455         #undef SLIST_FIELD
456
457         // clear list
458         me.nItems = 0;
459 }
460 void XonoticServerList_setSelected(entity me, float i)
461 {
462         float save;
463         save = me.selectedItem;
464         SUPER(XonoticServerList).setSelected(me, i);
465         /*
466         if(me.selectedItem == save)
467                 return;
468         */
469         if(me.nItems == 0)
470                 return;
471         if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
472                 return; // sorry, it would be wrong
473
474         if(me.selectedServer)
475                 strunzone(me.selectedServer);
476         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
477
478         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
479         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
480         me.ipAddressBoxFocused = -1;
481 }
482 void XonoticServerList_refreshServerList(entity me, float mode)
483 {
484         //print("refresh of type ", ftos(mode), "\n");
485
486         if(mode >= REFRESHSERVERLIST_REFILTER)
487         {
488                 float m, i, n;
489                 float listflags = 0;
490                 string s, typestr, modstr;
491
492                 s = me.filterString;
493
494                 m = strstrofs(s, ":", 0);
495                 if(m >= 0)
496                 {
497                         typestr = substring(s, 0, m);
498                         s = substring(s, m + 1, strlen(s) - m - 1);
499                         while(substring(s, 0, 1) == " ")
500                                 s = substring(s, 1, strlen(s) - 1);
501                 }
502                 else
503                         typestr = "";
504
505                 modstr = cvar_string("menu_slist_modfilter");
506
507                 m = SLIST_MASK_AND - 1;
508                 resethostcachemasks();
509
510                 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
511                 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
512
513                 // show full button
514                 if(!me.filterShowFull)
515                 {
516                         sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
517                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
518                 }
519
520                 // show empty button
521                 if(!me.filterShowEmpty)
522                         sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
523
524                 // gametype filtering
525                 if(typestr != "")
526                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
527
528                 // mod filtering
529                 if(modstr != "")
530                 {
531                         if(substring(modstr, 0, 1) == "!")
532                                 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
533                         else
534                                 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
535                 }
536
537                 // server banning
538                 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
539                 for(i = 0; i < n; ++i)
540                         if(argv(i) != "")
541                                 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
542
543                 m = SLIST_MASK_OR - 1;
544                 if(s != "")
545                 {
546                         sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
547                         sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
548                         sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
549                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
550                 }
551
552                 // sorting flags
553                 //listflags |= SLSF_FAVORITES;
554                 listflags |= SLSF_CATEGORIES;
555                 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
556                 sethostcachesort(me.currentSortField, listflags);
557         }
558         
559         resorthostcache();
560         if(mode >= REFRESHSERVERLIST_ASK)
561                 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
562 }
563 void XonoticServerList_focusEnter(entity me)
564 {
565         if(time < me.nextRefreshTime)
566         {
567                 //print("sorry, no refresh yet\n");
568                 return;
569         }
570         me.nextRefreshTime = time + 10;
571         me.refreshServerList(me, REFRESHSERVERLIST_ASK);
572 }
573
574 void XonoticServerList_draw(entity me)
575 {
576         float i, found, owned;
577
578         if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
579         {
580                 if(!me.needsRefresh)
581                         me.needsRefresh = 2;
582                 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
583         }
584
585         if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
586         {
587                 if(!me.needsRefresh)
588                         me.needsRefresh = 3;
589                 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
590         }
591
592         if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
593         {
594                 if(!me.needsRefresh)
595                         me.needsRefresh = 3;
596                 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
597         }
598
599         if(me.currentSortField == -1)
600         {
601                 me.setSortOrder(me, SLIST_FIELD_PING, +1);
602                 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
603         }
604         else if(me.needsRefresh == 1)
605         {
606                 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
607         }
608         else if(me.needsRefresh == 2)
609         {
610                 me.needsRefresh = 0;
611                 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
612         }
613         else if(me.needsRefresh == 3)
614         {
615                 me.needsRefresh = 0;
616                 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
617         }
618
619         owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
620
621         for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
622         category_draw_count = 0;
623
624         if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
625         {
626                 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
627                 me.nItems = itemcount;
628                 
629                 //float visible = floor(me.scrollPos / me.itemHeight);
630                 // ^ unfortunately no such optimization can be made-- we must process through the
631                 // entire list, otherwise there is no way to know which item is first in its category.
632
633                 // binary search method suggested by div
634                 float x;
635                 float begin = 0;
636                 for(x = 1; x <= category_ent_count; ++x) {
637                         float first = begin;
638                         float last = (itemcount - 1);
639                         if (first > last) {
640                                 // List is empty.
641                                 break;
642                         }
643                         float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
644                         float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
645                         if (catf > x) {
646                                 // The first one is already > x.
647                                 // Therefore, category x does not exist.
648                                 // Higher numbered categories do exist though.
649                         } else if (catl < x) {
650                                 // The last one is < x.
651                                 // Thus this category - and any following -
652                                 // don't exist.
653                                 break;
654                         } else if (catf == x) {
655                                 // Starts at first. This breaks the loop
656                                 // invariant in the binary search and thus has
657                                 // to be handled separately.
658                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
659                                         error("Category mismatch I");
660                                 if(first > 0)
661                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
662                                                 error("Category mismatch II");
663                                 category_name[category_draw_count] = x;
664                                 category_item[category_draw_count] = first;
665                                 ++category_draw_count;
666                                 begin = first + 1;
667                         } else {
668                                 // At this point, catf <= x < catl, thus
669                                 // catf < catl, thus first < last.
670                                 // INVARIANTS:
671                                 // last - first >= 1
672                                 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
673                                 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
674                                 // catf < x
675                                 // catl >= x
676                                 while (last - first > 1) {
677                                         float middle = floor((first + last) / 2);
678                                         // By loop condition, middle != first && middle != last.
679                                         float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
680                                         if (cat >= x) {
681                                                 last = middle;
682                                                 catl = cat;
683                                         } else {
684                                                 first = middle;
685                                                 catf = cat;
686                                         }
687                                 }
688                                 if (catl == x) {
689                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
690                                                 error("Category mismatch III");
691                                         if(last > 0)
692                                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
693                                                         error("Category mismatch IV");
694                                         category_name[category_draw_count] = x;
695                                         category_item[category_draw_count] = last;
696                                         ++category_draw_count;
697                                         begin = last + 1; // already scanned through these, skip 'em
698                                 }
699                                 else
700                                         begin = last; // already scanned through these, skip 'em
701                         }
702                 }
703                 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
704                 {
705                         category_name[0] = -1;
706                         category_item[0] = -1;
707                         category_draw_count = 0;
708                         me.nItems = itemcount;
709                 }
710         }
711         else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
712
713         me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
714         me.infoButton.disabled = ((me.nItems == 0) || !owned);
715         me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
716
717         found = 0;
718         if(me.selectedServer)
719         {
720                 for(i = 0; i < me.nItems; ++i)
721                 {
722                         if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
723                         {
724                                 if(i != me.selectedItem)
725                                 {
726                                         me.lastClickedServer = -1;
727                                         me.selectedItem = i;
728                                 }
729                                 found = 1;
730                                 break;
731                         }
732                 }
733         }
734         if(!found)
735         {
736                 if(me.nItems > 0)
737                 {
738                         if(me.selectedItem >= me.nItems)
739                                 me.selectedItem = me.nItems - 1;
740                         if(me.selectedServer)
741                                 strunzone(me.selectedServer);
742                         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
743                 }
744         }
745         
746         if(owned)
747         {
748                 if(me.selectedServer != me.ipAddressBox.text)
749                 {
750                         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
751                         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
752                         me.ipAddressBoxFocused = -1;
753                 }
754         }
755
756         if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
757         {
758                 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
759                         ServerList_Update_favoriteButton(NULL, me);
760                 me.ipAddressBoxFocused = me.ipAddressBox.focused;
761         }
762
763         SUPER(XonoticServerList).draw(me);
764 }
765 void ServerList_PingSort_Click(entity btn, entity me)
766 {
767         me.setSortOrder(me, SLIST_FIELD_PING, +1);
768 }
769 void ServerList_NameSort_Click(entity btn, entity me)
770 {
771         me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
772 }
773 void ServerList_MapSort_Click(entity btn, entity me)
774 {
775         me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
776 }
777 void ServerList_PlayerSort_Click(entity btn, entity me)
778 {
779         me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
780 }
781 void ServerList_TypeSort_Click(entity btn, entity me)
782 {
783         string s, t;
784         float i, m;
785         s = me.filterString;
786         m = strstrofs(s, ":", 0);
787         if(m >= 0)
788         {
789                 s = substring(s, 0, m);
790                 while(substring(s, m+1, 1) == " ") // skip spaces
791                         ++m;
792         }
793         else
794                 s = "";
795
796         for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
797         {
798                 t = MapInfo_Type_ToString(i);
799                 if(i > 1)
800                         if(t == "") // it repeats (default case)
801                         {
802                                 // no type was found
803                                 // choose the first one
804                                 s = MapInfo_Type_ToString(1);
805                                 break;
806                         }
807                 if(s == t)
808                 {
809                         // the type was found
810                         // choose the next one
811                         s = MapInfo_Type_ToString(i * 2);
812                         if(s == "")
813                                 s = MapInfo_Type_ToString(1);
814                         break;
815                 }
816         }
817
818         if(s != "")
819                 s = strcat(s, ":");
820         s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
821
822         me.controlledTextbox.setText(me.controlledTextbox, s);
823         me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
824         me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
825         //ServerList_Filter_Change(me.controlledTextbox, me);
826 }
827 void ServerList_Filter_Change(entity box, entity me)
828 {
829         if(me.filterString)
830                 strunzone(me.filterString);
831         if(box.text != "")
832                 me.filterString = strzone(box.text);
833         else
834                 me.filterString = string_null;
835         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
836
837         me.ipAddressBox.setText(me.ipAddressBox, "");
838         me.ipAddressBox.cursorPos = 0;
839         me.ipAddressBoxFocused = -1;
840 }
841 void ServerList_Categories_Click(entity box, entity me)
842 {
843         box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
844         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
845
846         me.ipAddressBox.setText(me.ipAddressBox, "");
847         me.ipAddressBox.cursorPos = 0;
848         me.ipAddressBoxFocused = -1;
849 }
850 void ServerList_ShowEmpty_Click(entity box, entity me)
851 {
852         box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
853         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
854
855         me.ipAddressBox.setText(me.ipAddressBox, "");
856         me.ipAddressBox.cursorPos = 0;
857         me.ipAddressBoxFocused = -1;
858 }
859 void ServerList_ShowFull_Click(entity box, entity me)
860 {
861         box.setChecked(box, me.filterShowFull = !me.filterShowFull);
862         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
863
864         me.ipAddressBox.setText(me.ipAddressBox, "");
865         me.ipAddressBox.cursorPos = 0;
866         me.ipAddressBoxFocused = -1;
867 }
868 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
869 {
870         if(me.currentSortField == fld)
871                 direction = -me.currentSortOrder;
872         me.currentSortOrder = direction;
873         me.currentSortField = fld;
874         me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
875         me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
876         me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
877         me.sortButton4.forcePressed = 0;
878         me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
879         me.selectedItem = 0;
880         if(me.selectedServer)
881                 strunzone(me.selectedServer);
882         me.selectedServer = string_null;
883         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
884 }
885 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
886 {
887         vector originInLBSpace, sizeInLBSpace;
888         originInLBSpace = eY * (-me.itemHeight);
889         sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
890
891         vector originInDialogSpace, sizeInDialogSpace;
892         originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
893         sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
894
895         btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
896         btn.Container_size_x   =                         sizeInDialogSpace_x * theSize;
897         btn.setText(btn, theTitle);
898         btn.onClick = theFunc;
899         btn.onClickEntity = me;
900         btn.resized = 1;
901 }
902 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
903 {
904         SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
905
906         me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
907         me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
908         me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
909
910         me.columnIconsOrigin = 0;
911         me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
912         me.columnPingSize = me.realFontSize_x * 3;
913         me.columnMapSize = me.realFontSize_x * 10;
914         me.columnTypeSize = me.realFontSize_x * 4;
915         me.columnPlayersSize = me.realFontSize_x * 5;
916         me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
917         me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
918         me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
919         me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
920         me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
921         me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
922
923         me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
924         me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
925         me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
926         me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
927         me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
928
929         float f;
930         f = me.currentSortField;
931         if(f >= 0)
932         {
933                 me.currentSortField = -1;
934                 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
935         }
936 }
937 void ServerList_Connect_Click(entity btn, entity me)
938 {
939         localcmd(sprintf("connect %s\n",
940                 ((me.ipAddressBox.text != "") ?
941                         me.ipAddressBox.text : me.selectedServer
942                 )
943         ));
944 }
945 void ServerList_Favorite_Click(entity btn, entity me)
946 {
947         string ipstr;
948         ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
949         if(ipstr != "")
950         {
951                 me.toggleFavorite(me, me.ipAddressBox.text);
952                 me.ipAddressBoxFocused = -1;
953         }
954 }
955 void ServerList_Info_Click(entity btn, entity me)
956 {
957         if (me.nItems != 0)
958                 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
959
960         vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
961         vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
962         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
963 }
964 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
965 {
966         if(i == me.lastClickedServer)
967                 if(time < me.lastClickedTime + 0.3)
968                 {
969                         // DOUBLE CLICK!
970                         ServerList_Connect_Click(NULL, me);
971                 }
972         me.lastClickedServer = i;
973         me.lastClickedTime = time;
974 }
975 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
976 {
977         // layout: Ping, Server name, Map name, NP, TP, MP
978         float p, q;
979         float isv4, isv6;
980         vector theColor;
981         float theAlpha;
982         float m, pure, freeslots, j, sflags;
983         string s, typestr, versionstr, k, v, modname;
984
985         //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
986
987         vector oldscale = draw_scale;
988         vector oldshift = draw_shift;
989 #define SET_YRANGE(start,end) \
990         draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
991         draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
992
993         for (j = 0; j < category_draw_count; ++j) {
994                 // Matches exactly the headings with increased height.
995                 if (i == category_item[j])
996                         break;
997         }
998
999         if (j < category_draw_count)
1000         {
1001                 entity catent = RetrieveCategoryEnt(category_name[j]);
1002                 if(catent)
1003                 {
1004                         SET_YRANGE(
1005                                 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1006                                 me.categoriesHeight / (me.categoriesHeight + 1)
1007                         );
1008                         draw_Text(
1009                                 eY * me.realUpperMargin
1010                                 +
1011 #if 0
1012                                 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1013                                 catent.cat_string,
1014 #else
1015                                 eX * (me.columnNameOrigin),
1016                                 strcat(catent.cat_string, ":"),
1017 #endif
1018                                 me.realFontSize,
1019                                 SKINCOLOR_SERVERLIST_CATEGORY,
1020                                 SKINALPHA_SERVERLIST_CATEGORY,
1021                                 0
1022                         );
1023                         SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1024                 }
1025         }
1026         
1027         if(isSelected)
1028                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1029
1030         s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1031         m = tokenizebyseparator(s, ":");
1032         typestr = "";
1033         if(m >= 2)
1034         {
1035                 typestr = argv(0);
1036                 versionstr = argv(1);
1037         }
1038         freeslots = -1;
1039         sflags = -1;
1040         modname = "";
1041         pure = 0;
1042         for(j = 2; j < m; ++j)
1043         {
1044                 if(argv(j) == "")
1045                         break;
1046                 k = substring(argv(j), 0, 1);
1047                 v = substring(argv(j), 1, -1);
1048                 if(k == "P")
1049                         pure = stof(v);
1050                 else if(k == "S")
1051                         freeslots = stof(v);
1052                 else if(k == "F")
1053                         sflags = stof(v);
1054                 else if(k == "M")
1055                         modname = v;
1056         }
1057
1058 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1059         if(modname == "")
1060                 modname = "Xonotic";
1061 #endif
1062
1063         /*
1064         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1065         s = gethostcachestring(SLIST_FIELD_MOD, i);
1066         if(s != "data")
1067                 if(modname == "Xonotic")
1068                         modname = s;
1069         */
1070
1071         // list the mods here on which the pure server check actually works
1072         if(modname != "Xonotic")
1073         if(modname != "MinstaGib")
1074         if(modname != "CTS")
1075         if(modname != "NIX")
1076         if(modname != "NewToys")
1077                 pure = 0;
1078
1079         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1080                 theAlpha = SKINALPHA_SERVERLIST_FULL;
1081         else if(freeslots == 0)
1082                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1083         else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1084                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1085         else
1086                 theAlpha = 1;
1087
1088         p = gethostcachenumber(SLIST_FIELD_PING, i);
1089 #define PING_LOW 75
1090 #define PING_MED 200
1091 #define PING_HIGH 500
1092         if(p < PING_LOW)
1093                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1094         else if(p < PING_MED)
1095                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1096         else if(p < PING_HIGH)
1097         {
1098                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1099                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1100         }
1101         else
1102         {
1103                 theColor = eX;
1104                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1105         }
1106
1107         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1108         {
1109                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1110                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1111         }
1112
1113         s = gethostcachestring(SLIST_FIELD_CNAME, i);
1114
1115         isv4 = isv6 = 0;
1116         if(substring(s, 0, 1) == "[")
1117         {
1118                 isv6 = 1;
1119                 me.seenIPv6 += 1;
1120         }
1121         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1122         {
1123                 isv4 = 1;
1124                 me.seenIPv4 += 1;
1125         }
1126
1127         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1128         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1129         {
1130                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1131                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1132         }
1133
1134         if(q == 1)
1135         {
1136                 if(cvar("crypto_aeslevel") >= 2)
1137                         q |= 4;
1138         }
1139         if(q == 2)
1140         {
1141                 if(cvar("crypto_aeslevel") >= 1)
1142                         q |= 4;
1143         }
1144         if(q == 3)
1145                 q = 5;
1146         else if(q >= 3)
1147                 q -= 2;
1148         // possible status:
1149         // 0: crypto off
1150         // 1: AES possible
1151         // 2: AES recommended but not available
1152         // 3: AES possible and will be used
1153         // 4: AES recommended and will be used
1154         // 5: AES required
1155
1156         // --------------
1157         //  RENDER ICONS
1158         // --------------
1159         vector iconSize = '0 0 0';
1160         iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1161         iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1162
1163         vector iconPos = '0 0 0';
1164         iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1165         iconPos_y = (1 - iconSize_y) * 0.5;
1166
1167         string n;
1168
1169         if (!(me.seenIPv4 && me.seenIPv6))
1170         {
1171                 iconPos_x += iconSize_x * 0.5;
1172         }
1173         else if(me.seenIPv4 && me.seenIPv6)
1174         {
1175                 n = string_null;
1176                 if(isv6)
1177                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1178                 else if(isv4)
1179                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1180                 if(n)
1181                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1182                 iconPos_x += iconSize_x;
1183         }
1184
1185         if(q > 0)
1186         {
1187                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1188                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1189         }
1190         iconPos_x += iconSize_x;
1191
1192         if(modname == "Xonotic")
1193         {
1194                 if(pure == 0)
1195                 {
1196                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1197                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1198                 }
1199         }
1200         else
1201         {
1202                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1203                 if(draw_PictureSize(n) == '0 0 0')
1204                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1205                 if(pure == 0)
1206                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1207                 else
1208                         draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1209         }
1210         iconPos_x += iconSize_x;
1211
1212         if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1213         {
1214                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1215                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1216         }
1217         iconPos_x += iconSize_x;
1218         
1219         // --------------
1220         //  RENDER TEXT
1221         // --------------
1222         
1223         // ping
1224         s = ftos(p);
1225         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1226
1227         // server name
1228         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1229         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1230
1231         // server map
1232         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1233         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1234
1235         // server gametype
1236         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1237         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1238
1239         // server playercount
1240         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1241         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1242 }
1243
1244 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1245 {
1246         vector org, sz;
1247
1248         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1249         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1250
1251         if(scan == K_ENTER || scan == K_KP_ENTER)
1252         {
1253                 ServerList_Connect_Click(NULL, me);
1254                 return 1;
1255         }
1256         else if(scan == K_MOUSE2 || scan == K_SPACE)
1257         {
1258                 if(me.nItems != 0)
1259                 {
1260                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1261                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1262                         return 1;
1263                 }
1264                 return 0;
1265         }
1266         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1267         {
1268                 if(me.nItems != 0)
1269                 {
1270                         me.toggleFavorite(me, me.selectedServer);
1271                         me.ipAddressBoxFocused = -1;
1272                         return 1;
1273                 }
1274                 return 0;
1275         }
1276         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1277                 return 1;
1278         else if(!me.controlledTextbox)
1279                 return 0;
1280         else
1281                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1282 }
1283
1284 float XonoticServerList_getTotalHeight(entity me) {
1285         float num_normal_rows = me.nItems;
1286         float num_headers = category_draw_count;
1287         return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1288 }
1289 float XonoticServerList_getItemAtPos(entity me, float pos) {
1290         pos = pos / me.itemHeight;
1291         float i;
1292         for (i = category_draw_count - 1; i >= 0; --i) {
1293                 float itemidx = category_item[i];
1294                 float itempos = i * me.categoriesHeight + category_item[i];
1295                 if (pos >= itempos + me.categoriesHeight + 1)
1296                         return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1297                 if (pos >= itempos)
1298                         return itemidx;
1299         }
1300         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1301         return floor(pos);
1302 }
1303 float XonoticServerList_getItemStart(entity me, float item) {
1304         float i;
1305         for (i = category_draw_count - 1; i >= 0; --i) {
1306                 float itemidx = category_item[i];
1307                 float itempos = i * me.categoriesHeight + category_item[i];
1308                 if (item >= itemidx + 1)
1309                         return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1310                 if (item >= itemidx)
1311                         return itempos * me.itemHeight;
1312         }
1313         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1314         return item * me.itemHeight;
1315 }
1316 float XonoticServerList_getItemHeight(entity me, float item) {
1317         float i;
1318         for (i = 0; i < category_draw_count; ++i) {
1319                 // Matches exactly the headings with increased height.
1320                 if (item == category_item[i])
1321                         return me.itemHeight * (me.categoriesHeight + 1);
1322         }
1323         return me.itemHeight;
1324 }
1325
1326 #endif