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