]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/menu/xonotic/serverlist.c
89947b00eac47e2e5f71c29fdb52327a99fd1b76
[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.25)
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") : _("Favorite")
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                         );
991                         draw_Text(
992                                 eY * me.realUpperMargin
993                                 +
994 #if 0
995                                 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
996                                 catent.cat_string,
997 #else
998                                 eX * (me.columnNameOrigin),
999                                 strcat(catent.cat_string, ":"),
1000 #endif
1001                                 me.realFontSize,
1002                                 '1 1 1',
1003                                 SKINALPHA_TEXT,
1004                                 0
1005                         );
1006                         SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1007                 }
1008         }
1009         
1010         if(isSelected)
1011                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1012
1013         s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1014         m = tokenizebyseparator(s, ":");
1015         typestr = "";
1016         if(m >= 2)
1017         {
1018                 typestr = argv(0);
1019                 versionstr = argv(1);
1020         }
1021         freeslots = -1;
1022         sflags = -1;
1023         modname = "";
1024         pure = 0;
1025         for(j = 2; j < m; ++j)
1026         {
1027                 if(argv(j) == "")
1028                         break;
1029                 k = substring(argv(j), 0, 1);
1030                 v = substring(argv(j), 1, -1);
1031                 if(k == "P")
1032                         pure = stof(v);
1033                 else if(k == "S")
1034                         freeslots = stof(v);
1035                 else if(k == "F")
1036                         sflags = stof(v);
1037                 else if(k == "M")
1038                         modname = v;
1039         }
1040
1041 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1042         if(modname == "")
1043                 modname = "Xonotic";
1044 #endif
1045
1046         /*
1047         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1048         s = gethostcachestring(SLIST_FIELD_MOD, i);
1049         if(s != "data")
1050                 if(modname == "Xonotic")
1051                         modname = s;
1052         */
1053
1054         // list the mods here on which the pure server check actually works
1055         if(modname != "Xonotic")
1056         if(modname != "MinstaGib")
1057         if(modname != "CTS")
1058         if(modname != "NIX")
1059         if(modname != "NewToys")
1060                 pure = 0;
1061
1062         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1063                 theAlpha = SKINALPHA_SERVERLIST_FULL;
1064         else if(freeslots == 0)
1065                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1066         else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1067                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1068         else
1069                 theAlpha = 1;
1070
1071         p = gethostcachenumber(SLIST_FIELD_PING, i);
1072 #define PING_LOW 75
1073 #define PING_MED 200
1074 #define PING_HIGH 500
1075         if(p < PING_LOW)
1076                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1077         else if(p < PING_MED)
1078                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1079         else if(p < PING_HIGH)
1080         {
1081                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1082                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1083         }
1084         else
1085         {
1086                 theColor = eX;
1087                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1088         }
1089
1090         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1091         {
1092                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1093                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1094         }
1095
1096         s = gethostcachestring(SLIST_FIELD_CNAME, i);
1097
1098         isv4 = isv6 = 0;
1099         if(substring(s, 0, 1) == "[")
1100         {
1101                 isv6 = 1;
1102                 me.seenIPv6 += 1;
1103         }
1104         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1105         {
1106                 isv4 = 1;
1107                 me.seenIPv4 += 1;
1108         }
1109
1110         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1111         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1112         {
1113                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1114                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1115         }
1116
1117         if(q == 1)
1118         {
1119                 if(cvar("crypto_aeslevel") >= 2)
1120                         q |= 4;
1121         }
1122         if(q == 2)
1123         {
1124                 if(cvar("crypto_aeslevel") >= 1)
1125                         q |= 4;
1126         }
1127         if(q == 3)
1128                 q = 5;
1129         else if(q >= 3)
1130                 q -= 2;
1131         // possible status:
1132         // 0: crypto off
1133         // 1: AES possible
1134         // 2: AES recommended but not available
1135         // 3: AES possible and will be used
1136         // 4: AES recommended and will be used
1137         // 5: AES required
1138
1139         // --------------
1140         //  RENDER ICONS
1141         // --------------
1142         vector iconSize = '0 0 0';
1143         iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1144         iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1145
1146         vector iconPos = '0 0 0';
1147         iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1148         iconPos_y = (1 - iconSize_y) * 0.5;
1149
1150         string n;
1151
1152         if not(me.seenIPv4 && me.seenIPv6)
1153         {
1154                 iconPos_x += iconSize_x * 0.5;
1155         }
1156         else if(me.seenIPv4 && me.seenIPv6)
1157         {
1158                 n = string_null;
1159                 if(isv6)
1160                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1161                 else if(isv4)
1162                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1163                 if(n)
1164                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1165                 iconPos_x += iconSize_x;
1166         }
1167
1168         if(q > 0)
1169         {
1170                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1171                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1172         }
1173         iconPos_x += iconSize_x;
1174
1175         if(modname == "Xonotic")
1176         {
1177                 if(pure == 0)
1178                 {
1179                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1180                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1181                 }
1182         }
1183         else
1184         {
1185                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1186                 if(draw_PictureSize(n) == '0 0 0')
1187                         draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1188                 if(pure == 0)
1189                         draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1190                 else
1191                         draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1192         }
1193         iconPos_x += iconSize_x;
1194
1195         if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1196         {
1197                 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1198                 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1199         }
1200         iconPos_x += iconSize_x;
1201         
1202         // --------------
1203         //  RENDER TEXT
1204         // --------------
1205         
1206         // ping
1207         s = ftos(p);
1208         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1209
1210         // server name
1211         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1212         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1213
1214         // server map
1215         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1216         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1217
1218         // server gametype
1219         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1220         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1221
1222         // server playercount
1223         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1224         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1225 }
1226
1227 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1228 {
1229         float i = me.selectedItem;
1230         vector org, sz;
1231
1232         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1233         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1234
1235         me.lastBumpSelectTime = 0;
1236
1237         if(scan == K_ENTER || scan == K_KP_ENTER)
1238         {
1239                 ServerList_Connect_Click(NULL, me);
1240                 return 1;
1241         }
1242         else if(scan == K_MOUSE2 || scan == K_SPACE)
1243         {
1244                 if(me.nItems != 0)
1245                 {
1246                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1247                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1248                         return 1;
1249                 }
1250                 return 0;
1251         }
1252         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1253         {
1254                 if(me.nItems != 0)
1255                 {
1256                         me.toggleFavorite(me, me.selectedServer);
1257                         me.ipAddressBoxFocused = -1;
1258                         return 1;
1259                 }
1260                 return 0;
1261         }
1262         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1263                 return 1;
1264         else if(!me.controlledTextbox)
1265                 return 0;
1266         else
1267                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1268 }
1269
1270 float XonoticServerList_getTotalHeight(entity me) {
1271         float num_normal_rows = me.nItems;
1272         float num_headers = category_draw_count;
1273         return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1274 }
1275 float XonoticServerList_getItemAtPos(entity me, float pos) {
1276         pos = pos / me.itemHeight;
1277         float i;
1278         for (i = category_draw_count - 1; i >= 0; --i) {
1279                 float itemidx = category_item[i];
1280                 float itempos = i * me.categoriesHeight + category_item[i];
1281                 if (pos >= itempos + me.categoriesHeight + 1)
1282                         return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1283                 if (pos >= itempos)
1284                         return itemidx;
1285         }
1286         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1287         return floor(pos);
1288 }
1289 float XonoticServerList_getItemStart(entity me, float item) {
1290         float i;
1291         for (i = category_draw_count - 1; i >= 0; --i) {
1292                 float itemidx = category_item[i];
1293                 float itempos = i * me.categoriesHeight + category_item[i];
1294                 if (item >= itemidx + 1)
1295                         return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1296                 if (item >= itemidx)
1297                         return itempos * me.itemHeight;
1298         }
1299         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1300         return item * me.itemHeight;
1301 }
1302 float XonoticServerList_getItemHeight(entity me, float item) {
1303         float i;
1304         for (i = 0; i < category_draw_count; ++i) {
1305                 // Matches exactly the headings with increased height.
1306                 if (item == category_item[i])
1307                         return me.itemHeight * (me.categoriesHeight + 1);
1308         }
1309         return me.itemHeight;
1310 }
1311
1312 #endif