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