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