Merge branch 'terencehill/menu_listbox_changes' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / xonotic / serverlist.qc
1 #ifndef SERVERLIST_H
2 #define SERVERLIST_H
3 #include "listbox.qc"
4 CLASS(XonoticServerList, XonoticListBox)
5         METHOD(XonoticServerList, configureXonoticServerList, void(entity))
6         ATTRIB(XonoticServerList, rowsPerItem, float, 1)
7         METHOD(XonoticServerList, draw, void(entity))
8         METHOD(XonoticServerList, drawListBoxItem, void(entity, int, vector, bool, bool))
9         METHOD(XonoticServerList, doubleClickListBoxItem, void(entity, float, vector))
10         METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
11         METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
12         METHOD(XonoticServerList, toggleFavorite, void(entity, string))
13
14         ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
15
16         ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
17         ATTRIB(XonoticServerList, realUpperMargin, float, 0)
18         ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
19         ATTRIB(XonoticServerList, columnIconsSize, float, 0)
20         ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
21         ATTRIB(XonoticServerList, columnPingSize, float, 0)
22         ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
23         ATTRIB(XonoticServerList, columnNameSize, float, 0)
24         ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
25         ATTRIB(XonoticServerList, columnMapSize, float, 0)
26         ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
27         ATTRIB(XonoticServerList, columnTypeSize, float, 0)
28         ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
29         ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
30         ATTRIB(XonoticServerList, lockedSelectedItem, bool, true) // initially keep selected the first item of the list, avoiding an unwanted scrolling
31
32         ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
33         METHOD(XonoticServerList, setSelected, void(entity, float))
34         METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
35         ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
36         ATTRIB(XonoticServerList, filterShowFull, float, 1)
37         ATTRIB(XonoticServerList, filterString, string, string_null)
38         ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
39         ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
40         ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
41         ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
42         METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
43         ATTRIB(XonoticServerList, needsRefresh, float, 1)
44         METHOD(XonoticServerList, focusEnter, void(entity))
45         METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
46         ATTRIB(XonoticServerList, sortButton1, entity, NULL)
47         ATTRIB(XonoticServerList, sortButton2, entity, NULL)
48         ATTRIB(XonoticServerList, sortButton3, entity, NULL)
49         ATTRIB(XonoticServerList, sortButton4, entity, NULL)
50         ATTRIB(XonoticServerList, sortButton5, entity, NULL)
51         ATTRIB(XonoticServerList, connectButton, entity, NULL)
52         ATTRIB(XonoticServerList, infoButton, entity, NULL)
53         ATTRIB(XonoticServerList, currentSortOrder, float, 0)
54         ATTRIB(XonoticServerList, currentSortField, float, -1)
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 float autocvar_menu_slist_categories;
71 float autocvar_menu_slist_categories_onlyifmultiple;
72 float autocvar_menu_slist_purethreshold;
73 float autocvar_menu_slist_modimpurity;
74 float autocvar_menu_slist_recommendations;
75 float autocvar_menu_slist_recommendations_maxping;
76 float autocvar_menu_slist_recommendations_minfreeslots;
77 float autocvar_menu_slist_recommendations_minhumans;
78 float autocvar_menu_slist_recommendations_purethreshold;
79
80 // server cache fields
81 #define SLIST_FIELDS \
82         SLIST_FIELD(CNAME,       "cname") \
83         SLIST_FIELD(PING,        "ping") \
84         SLIST_FIELD(GAME,        "game") \
85         SLIST_FIELD(MOD,         "mod") \
86         SLIST_FIELD(MAP,         "map") \
87         SLIST_FIELD(NAME,        "name") \
88         SLIST_FIELD(MAXPLAYERS,  "maxplayers") \
89         SLIST_FIELD(NUMPLAYERS,  "numplayers") \
90         SLIST_FIELD(NUMHUMANS,   "numhumans") \
91         SLIST_FIELD(NUMBOTS,     "numbots") \
92         SLIST_FIELD(PROTOCOL,    "protocol") \
93         SLIST_FIELD(FREESLOTS,   "freeslots") \
94         SLIST_FIELD(PLAYERS,     "players") \
95         SLIST_FIELD(QCSTATUS,    "qcstatus") \
96         SLIST_FIELD(CATEGORY,    "category") \
97         SLIST_FIELD(ISFAVORITE,  "isfavorite")
98
99 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
100 SLIST_FIELDS
101 #undef SLIST_FIELD
102
103 const float REFRESHSERVERLIST_RESORT = 0;    // sort the server list again to update for changes to e.g. favorite status, categories
104 const float REFRESHSERVERLIST_REFILTER = 1;  // ..., also update filter and sort criteria
105 const float REFRESHSERVERLIST_ASK = 2;       // ..., also suggest querying servers now
106 const float REFRESHSERVERLIST_RESET = 3;     // ..., also clear the list first
107
108 // function declarations
109 float IsServerInList(string list, string srv);
110 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
111 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
112 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
113
114 entity RetrieveCategoryEnt(float catnum);
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 const int MAX_CATEGORIES = 9;
133 const int CATEGORY_FIRST = 1;
134 entity categories[MAX_CATEGORIES];
135 int 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 int category_name[MAX_CATEGORIES];
145 int category_item[MAX_CATEGORIES];
146 int category_draw_count;
147
148 #define SLIST_CATEGORIES \
149         SLIST_CATEGORY(CAT_FAVORITED,    "",            "",             ZCTX(_("SLCAT^Favorites"))) \
150         SLIST_CATEGORY(CAT_RECOMMENDED,  "",            "",             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_INSTAGIB,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^InstaGib 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         int name; \
162         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         int 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                                         printf( \
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(int 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 bool IsServerInList(string list, string srv)
239 {
240         string p;
241         int 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 int CheckCategoryOverride(int cat)
267 {
268         entity catent = RetrieveCategoryEnt(cat);
269         if(catent)
270         {
271                 int 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 int CheckCategoryForEntry(int entry)
283 {
284         string s, k, v, modtype = "";
285         int 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                 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
312
313                 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
314                 else
315                 {
316                         float recommended = 0;
317                         if(autocvar_menu_slist_recommendations & 1)
318                         {
319                                 if(IsRecommended(cname)) { ++recommended; }
320                                 else { --recommended; }
321                         }
322                         if(autocvar_menu_slist_recommendations & 2)
323                         {
324                                 if(
325                                         ///// check for minimum free slots
326                                         (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
327
328                                         && // check for purity requirement
329                                         (
330                                                 (autocvar_menu_slist_recommendations_purethreshold < 0)
331                                                 ||
332                                                 (impure <= autocvar_menu_slist_recommendations_purethreshold)
333                                         )
334
335                                         && // check for minimum amount of humans
336                                         (
337                                                 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
338                                                 >=
339                                                 autocvar_menu_slist_recommendations_minhumans
340                                         )
341
342                                         && // check for maximum latency
343                                         (
344                                                 gethostcachenumber(SLIST_FIELD_PING, entry)
345                                                 <=
346                                                 autocvar_menu_slist_recommendations_maxping
347                                         )
348                                 )
349                                         { ++recommended; }
350                                 else
351                                         { --recommended; }
352                         }
353                         if(recommended > 0) { return CAT_RECOMMENDED; }
354                 }
355         }
356
357         // if not favorited or recommended, check modname
358         if(modtype != "xonotic")
359         {
360                 switch(modtype)
361                 {
362                         // old servers which don't report their mod name are considered modified now
363                         case "": { return CAT_MODIFIED; }
364
365                         case "xpm": { return CAT_XPM; }
366                         case "minstagib":
367                         case "instagib": { return CAT_INSTAGIB; }
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: { dprintf("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         int i, n;
388         bool f = false;
389         srv_resolved = netaddress_resolve(srv, 26000);
390         p = crypto_getidfp(srv_resolved);
391         s = cvar_string("net_slist_favorites");
392         n = tokenize_console(s);
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 = true;
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 = NEW(XonoticServerList);
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, int i)
463 {
464         me.lockedSelectedItem = false;
465         //int 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, int mode)
485 {
486         //print("refresh of type ", ftos(mode), "\n");
487
488         if(mode >= REFRESHSERVERLIST_REFILTER)
489         {
490                 float m;
491                 int i, n;
492                 int listflags = 0;
493                 string s, typestr, modstr;
494
495                 s = me.filterString;
496
497                 m = strstrofs(s, ":", 0);
498                 if(m >= 0)
499                 {
500                         typestr = substring(s, 0, m);
501                         s = substring(s, m + 1, strlen(s) - m - 1);
502                         while(substring(s, 0, 1) == " ")
503                                 s = substring(s, 1, strlen(s) - 1);
504                 }
505                 else
506                         typestr = "";
507
508                 modstr = cvar_string("menu_slist_modfilter");
509
510                 m = SLIST_MASK_AND - 1;
511                 resethostcachemasks();
512
513                 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
514                 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
515
516                 // show full button
517                 if(!me.filterShowFull)
518                 {
519                         sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
520                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
521                 }
522
523                 // show empty button
524                 if(!me.filterShowEmpty)
525                         sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
526
527                 // gametype filtering
528                 if(typestr != "")
529                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
530
531                 // mod filtering
532                 if(modstr != "")
533                 {
534                         if(substring(modstr, 0, 1) == "!")
535                                 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
536                         else
537                                 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
538                 }
539
540                 // server banning
541                 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
542                 for(i = 0; i < n; ++i)
543                         if(argv(i) != "")
544                                 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
545
546                 m = SLIST_MASK_OR - 1;
547                 if(s != "")
548                 {
549                         sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
550                         sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
551                         sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
552                         sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
553                 }
554
555                 // sorting flags
556                 //listflags |= SLSF_FAVORITES;
557                 listflags |= SLSF_CATEGORIES;
558                 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
559                 sethostcachesort(me.currentSortField, listflags);
560         }
561
562         resorthostcache();
563         if(mode >= REFRESHSERVERLIST_ASK)
564                 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
565 }
566 void XonoticServerList_focusEnter(entity me)
567 {
568         SUPER(XonoticServerList).focusEnter(me);
569         if(time < me.nextRefreshTime)
570         {
571                 //print("sorry, no refresh yet\n");
572                 return;
573         }
574         me.nextRefreshTime = time + 10;
575         me.refreshServerList(me, REFRESHSERVERLIST_ASK);
576 }
577
578 void XonoticServerList_draw(entity me)
579 {
580         int i;
581         bool found = false, owned;
582
583         if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
584         {
585                 if(!me.needsRefresh)
586                         me.needsRefresh = 2;
587                 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
588         }
589
590         if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
591         {
592                 if(!me.needsRefresh)
593                         me.needsRefresh = 3;
594                 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
595         }
596
597         if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
598         {
599                 if(!me.needsRefresh)
600                         me.needsRefresh = 3;
601                 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
602         }
603
604         if(me.currentSortField == -1)
605         {
606                 me.setSortOrder(me, SLIST_FIELD_PING, +1);
607                 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
608         }
609         else if(me.needsRefresh == 1)
610         {
611                 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
612         }
613         else if(me.needsRefresh == 2)
614         {
615                 me.needsRefresh = 0;
616                 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
617         }
618         else if(me.needsRefresh == 3)
619         {
620                 me.needsRefresh = 0;
621                 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
622         }
623
624         owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
625
626         for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
627         category_draw_count = 0;
628
629         if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
630         {
631                 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
632                 me.nItems = itemcount;
633
634                 //float visible = floor(me.scrollPos / me.itemHeight);
635                 // ^ unfortunately no such optimization can be made-- we must process through the
636                 // entire list, otherwise there is no way to know which item is first in its category.
637
638                 // binary search method suggested by div
639                 float x;
640                 float begin = 0;
641                 for(x = 1; x <= category_ent_count; ++x) {
642                         float first = begin;
643                         float last = (itemcount - 1);
644                         if (first > last) {
645                                 // List is empty.
646                                 break;
647                         }
648                         float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
649                         float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
650                         if (catf > x) {
651                                 // The first one is already > x.
652                                 // Therefore, category x does not exist.
653                                 // Higher numbered categories do exist though.
654                         } else if (catl < x) {
655                                 // The last one is < x.
656                                 // Thus this category - and any following -
657                                 // don't exist.
658                                 break;
659                         } else if (catf == x) {
660                                 // Starts at first. This breaks the loop
661                                 // invariant in the binary search and thus has
662                                 // to be handled separately.
663                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
664                                         error("Category mismatch I");
665                                 if(first > 0)
666                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
667                                                 error("Category mismatch II");
668                                 category_name[category_draw_count] = x;
669                                 category_item[category_draw_count] = first;
670                                 ++category_draw_count;
671                                 begin = first + 1;
672                         } else {
673                                 // At this point, catf <= x < catl, thus
674                                 // catf < catl, thus first < last.
675                                 // INVARIANTS:
676                                 // last - first >= 1
677                                 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
678                                 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
679                                 // catf < x
680                                 // catl >= x
681                                 while (last - first > 1) {
682                                         float middle = floor((first + last) / 2);
683                                         // By loop condition, middle != first && middle != last.
684                                         float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
685                                         if (cat >= x) {
686                                                 last = middle;
687                                                 catl = cat;
688                                         } else {
689                                                 first = middle;
690                                                 catf = cat;
691                                         }
692                                 }
693                                 if (catl == x) {
694                                         if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
695                                                 error("Category mismatch III");
696                                         if(last > 0)
697                                                 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
698                                                         error("Category mismatch IV");
699                                         category_name[category_draw_count] = x;
700                                         category_item[category_draw_count] = last;
701                                         ++category_draw_count;
702                                         begin = last + 1; // already scanned through these, skip 'em
703                                 }
704                                 else
705                                         begin = last; // already scanned through these, skip 'em
706                         }
707                 }
708                 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
709                 {
710                         category_name[0] = -1;
711                         category_item[0] = -1;
712                         category_draw_count = 0;
713                         me.nItems = itemcount;
714                 }
715         }
716         else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
717
718         me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
719         me.infoButton.disabled = ((me.nItems == 0) || !owned);
720         me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
721
722         if(me.lockedSelectedItem)
723         {
724                 if(me.nItems > 0)
725                 {
726                         if(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem) != me.selectedServer)
727                         {
728                                 if(me.selectedServer)
729                                         strunzone(me.selectedServer);
730                                 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
731                         }
732                         found = true;
733                 }
734         }
735         else if(me.selectedServer)
736         {
737                 for(i = 0; i < me.nItems; ++i)
738                 {
739                         if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
740                         {
741                                 // don't follow the selected item with SUPER(XonoticServerList).setSelected(me, i);
742                                 me.selectedItem = i;
743                                 found = true;
744                                 break;
745                         }
746                 }
747         }
748         if(!found)
749         {
750                 if(me.nItems > 0)
751                 {
752                         // selected server disappeared, select the last server (scrolling to it)
753                         if(me.selectedItem >= me.nItems)
754                                 SUPER(XonoticServerList).setSelected(me, me.nItems - 1);
755                         if(me.selectedServer)
756                                 strunzone(me.selectedServer);
757                         me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
758                 }
759         }
760
761         if(owned)
762         {
763                 if(me.selectedServer != me.ipAddressBox.text)
764                 {
765                         me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
766                         me.ipAddressBox.cursorPos = strlen(me.selectedServer);
767                         me.ipAddressBoxFocused = -1;
768                 }
769         }
770
771         if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
772         {
773                 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
774                         ServerList_Update_favoriteButton(NULL, me);
775                 me.ipAddressBoxFocused = me.ipAddressBox.focused;
776         }
777
778         SUPER(XonoticServerList).draw(me);
779 }
780 void ServerList_PingSort_Click(entity btn, entity me)
781 {
782         me.setSortOrder(me, SLIST_FIELD_PING, +1);
783 }
784 void ServerList_NameSort_Click(entity btn, entity me)
785 {
786         me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
787 }
788 void ServerList_MapSort_Click(entity btn, entity me)
789 {
790         me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
791 }
792 void ServerList_PlayerSort_Click(entity btn, entity me)
793 {
794         me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
795 }
796 void ServerList_TypeSort_Click(entity btn, entity me)
797 {
798         string s, t;
799         float i, m;
800         s = me.filterString;
801         m = strstrofs(s, ":", 0);
802         if(m >= 0)
803         {
804                 s = substring(s, 0, m);
805                 while(substring(s, m+1, 1) == " ") // skip spaces
806                         ++m;
807         }
808         else
809                 s = "";
810
811         for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
812         {
813                 t = MapInfo_Type_ToString(i);
814                 if(i > 1)
815                         if(t == "") // it repeats (default case)
816                         {
817                                 // no type was found
818                                 // choose the first one
819                                 s = MapInfo_Type_ToString(1);
820                                 break;
821                         }
822                 if(s == t)
823                 {
824                         // the type was found
825                         // choose the next one
826                         s = MapInfo_Type_ToString(i * 2);
827                         if(s == "")
828                                 s = MapInfo_Type_ToString(1);
829                         break;
830                 }
831         }
832
833         if(s != "")
834                 s = strcat(s, ":");
835         s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
836
837         me.controlledTextbox.setText(me.controlledTextbox, s);
838         me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
839         me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
840         //ServerList_Filter_Change(me.controlledTextbox, me);
841 }
842 void ServerList_Filter_Change(entity box, entity me)
843 {
844         if(me.filterString)
845                 strunzone(me.filterString);
846         if(box.text != "")
847                 me.filterString = strzone(box.text);
848         else
849                 me.filterString = string_null;
850         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
851
852         me.ipAddressBox.setText(me.ipAddressBox, "");
853         me.ipAddressBox.cursorPos = 0;
854         me.ipAddressBoxFocused = -1;
855 }
856 void ServerList_Categories_Click(entity box, entity me)
857 {
858         box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
859         me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
860
861         me.ipAddressBox.setText(me.ipAddressBox, "");
862         me.ipAddressBox.cursorPos = 0;
863         me.ipAddressBoxFocused = -1;
864 }
865 void ServerList_ShowEmpty_Click(entity box, entity me)
866 {
867         box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
868         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
869
870         me.ipAddressBox.setText(me.ipAddressBox, "");
871         me.ipAddressBox.cursorPos = 0;
872         me.ipAddressBoxFocused = -1;
873 }
874 void ServerList_ShowFull_Click(entity box, entity me)
875 {
876         box.setChecked(box, me.filterShowFull = !me.filterShowFull);
877         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
878
879         me.ipAddressBox.setText(me.ipAddressBox, "");
880         me.ipAddressBox.cursorPos = 0;
881         me.ipAddressBoxFocused = -1;
882 }
883 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
884 {
885         if(me.currentSortField == fld)
886                 direction = -me.currentSortOrder;
887         me.currentSortOrder = direction;
888         me.currentSortField = fld;
889         me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
890         me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
891         me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
892         me.sortButton4.forcePressed = 0;
893         me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
894         me.selectedItem = 0;
895         if(me.selectedServer)
896                 strunzone(me.selectedServer);
897         me.selectedServer = string_null;
898         me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
899 }
900 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
901 {
902         vector originInLBSpace, sizeInLBSpace;
903         originInLBSpace = eY * (-me.itemHeight);
904         sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
905
906         vector originInDialogSpace, sizeInDialogSpace;
907         originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
908         sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
909
910         btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
911         btn.Container_size_x   =                         sizeInDialogSpace.x * theSize;
912         btn.setText(btn, theTitle);
913         btn.onClick = theFunc;
914         btn.onClickEntity = me;
915         btn.resized = 1;
916 }
917 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
918 {
919         SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
920
921         me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
922         me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
923         me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
924
925         me.columnIconsOrigin = 0;
926         me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
927         me.columnPingSize = me.realFontSize.x * 3;
928         me.columnMapSize = me.realFontSize.x * 10;
929         me.columnTypeSize = me.realFontSize.x * 4;
930         me.columnPlayersSize = me.realFontSize.x * 5;
931         me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
932         me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
933         me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
934         me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
935         me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
936         me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
937
938         me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
939         me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
940         me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
941         me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
942         me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
943
944         int f = me.currentSortField;
945         if(f >= 0)
946         {
947                 me.currentSortField = -1;
948                 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
949         }
950 }
951 void ServerList_Connect_Click(entity btn, entity me)
952 {
953         localcmd(sprintf("connect %s\n",
954                 ((me.ipAddressBox.text != "") ?
955                         me.ipAddressBox.text : me.selectedServer
956                 )
957         ));
958 }
959 void ServerList_Favorite_Click(entity btn, entity me)
960 {
961         string ipstr;
962         ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
963         if(ipstr != "")
964         {
965                 m_play_click_sound(MENU_SOUND_SELECT);
966                 me.toggleFavorite(me, me.ipAddressBox.text);
967                 me.ipAddressBoxFocused = -1;
968         }
969 }
970 void ServerList_Info_Click(entity btn, entity me)
971 {
972         if (me.nItems != 0)
973                 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
974
975         vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
976         vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
977         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
978 }
979 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
980 {
981         ServerList_Connect_Click(NULL, me);
982 }
983 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
984 {
985         // layout: Ping, Server name, Map name, NP, TP, MP
986         float p;
987         int q;
988         bool isv4, isv6;
989         vector theColor;
990         float theAlpha;
991         bool pure = false;
992         int freeslots = -1, sflags = -1, j, m;
993         string s, typestr, versionstr, k, v, modname;
994
995         //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
996
997         vector oldscale = draw_scale;
998         vector oldshift = draw_shift;
999 #define SET_YRANGE(start,end) \
1000         draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
1001         draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
1002
1003         for (j = 0; j < category_draw_count; ++j) {
1004                 // Matches exactly the headings with increased height.
1005                 if (i == category_item[j])
1006                         break;
1007         }
1008
1009         if (j < category_draw_count)
1010         {
1011                 entity catent = RetrieveCategoryEnt(category_name[j]);
1012                 if(catent)
1013                 {
1014                         SET_YRANGE(
1015                                 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1016                                 me.categoriesHeight / (me.categoriesHeight + 1)
1017                         );
1018                         draw_Text(
1019                                 eY * me.realUpperMargin
1020                                 +
1021 #if 0
1022                                 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1023                                 catent.cat_string,
1024 #else
1025                                 eX * (me.columnNameOrigin),
1026                                 strcat(catent.cat_string, ":"),
1027 #endif
1028                                 me.realFontSize,
1029                                 SKINCOLOR_SERVERLIST_CATEGORY,
1030                                 SKINALPHA_SERVERLIST_CATEGORY,
1031                                 0
1032                         );
1033                         SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1034                 }
1035         }
1036
1037         if(isSelected)
1038                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1039         else if(isFocused)
1040         {
1041                 me.focusedItemAlpha = getFadedAlpha(me.focusedItemAlpha, SKINALPHA_LISTBOX_FOCUSED, SKINFADEALPHA_LISTBOX_FOCUSED);
1042                 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_FOCUSED, me.focusedItemAlpha);
1043         }
1044
1045         s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1046         m = tokenizebyseparator(s, ":");
1047         typestr = "";
1048         if(m >= 2)
1049         {
1050                 typestr = argv(0);
1051                 versionstr = argv(1);
1052         }
1053         freeslots = -1;
1054         modname = "";
1055         for(j = 2; j < m; ++j)
1056         {
1057                 if(argv(j) == "")
1058                         break;
1059                 k = substring(argv(j), 0, 1);
1060                 v = substring(argv(j), 1, -1);
1061                 if(k == "P")
1062                         pure = stob(v);
1063                 else if(k == "S")
1064                         freeslots = stof(v);
1065                 else if(k == "F")
1066                         sflags = stoi(v);
1067                 else if(k == "M")
1068                         modname = v;
1069         }
1070
1071 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1072         if(modname == "")
1073                 modname = "Xonotic";
1074 #endif
1075
1076         /*
1077         SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1078         s = gethostcachestring(SLIST_FIELD_MOD, i);
1079         if(s != "data")
1080                 if(modname == "Xonotic")
1081                         modname = s;
1082         */
1083
1084         // list the mods here on which the pure server check actually works
1085         if(modname != "Xonotic")
1086         if(modname != "InstaGib" || modname != "MinstaGib")
1087         if(modname != "CTS")
1088         if(modname != "NIX")
1089         if(modname != "NewToys")
1090                 pure = false;
1091
1092         if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1093                 theAlpha = SKINALPHA_SERVERLIST_FULL;
1094         else if(freeslots == 0)
1095                 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1096         else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1097                 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1098         else
1099                 theAlpha = 1;
1100
1101         p = gethostcachenumber(SLIST_FIELD_PING, i);
1102         const int PING_LOW = 75;
1103         const int PING_MED = 200;
1104         const int PING_HIGH = 500;
1105         if(p < PING_LOW)
1106                 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1107         else if(p < PING_MED)
1108                 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1109         else if(p < PING_HIGH)
1110         {
1111                 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1112                 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1113         }
1114         else
1115         {
1116                 theColor = eX;
1117                 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1118         }
1119
1120         if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1121         {
1122                 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1123                 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1124         }
1125
1126         s = gethostcachestring(SLIST_FIELD_CNAME, i);
1127
1128         isv4 = isv6 = false;
1129         if(substring(s, 0, 1) == "[")
1130         {
1131                 isv6 = true;
1132                 me.seenIPv6 += 1;
1133         }
1134         else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1135         {
1136                 isv4 = true;
1137                 me.seenIPv4 += 1;
1138         }
1139
1140         q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1141         if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1142         {
1143                 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1144                 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1145         }
1146
1147         if(q == 1)
1148         {
1149                 if(cvar("crypto_aeslevel") >= 2)
1150                         q |= 4;
1151         }
1152         if(q == 2)
1153         {
1154                 if(cvar("crypto_aeslevel") >= 1)
1155                         q |= 4;
1156         }
1157         if(q == 3)
1158                 q = 5;
1159         else if(q >= 3)
1160                 q -= 2;
1161         // possible status:
1162         // 0: crypto off
1163         // 1: AES possible
1164         // 2: AES recommended but not available
1165         // 3: AES possible and will be used
1166         // 4: AES recommended and will be used
1167         // 5: AES required
1168
1169         // --------------
1170         //  RENDER ICONS
1171         // --------------
1172         vector iconSize = '0 0 0';
1173         iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1174         iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1175
1176         vector iconPos = '0 0 0';
1177         iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1178         iconPos_y = (1 - iconSize.y) * 0.5;
1179
1180         // IP
1181         if(me.seenIPv4 && me.seenIPv6)
1182         {
1183                 if(isv6)
1184                         draw_Picture(iconPos, "icon_ipv6", iconSize, '1 1 1', 1);
1185                 else if(isv4)
1186                         draw_Picture(iconPos, "icon_ipv4", iconSize, '1 1 1', 1);
1187         }
1188
1189         iconPos.x += iconSize.x;
1190
1191         // AES
1192         if(q > 0)
1193                 draw_Picture(iconPos, strcat("icon_aeslevel", ftos(q)), iconSize, '1 1 1', 1);
1194
1195         iconPos.x += iconSize.x;
1196
1197         // Mod
1198         if(modname == "Xonotic")
1199         {
1200                 if(pure == 0)
1201                         draw_Picture(iconPos, "icon_pure1", iconSize, '1 1 1', 1);
1202         }
1203         else
1204         {
1205                 string icon = strcat("icon_mod_", modname);
1206                 if(draw_PictureSize(icon) == '0 0 0')
1207                         icon = "icon_mod_";
1208
1209                 if(pure == 0)
1210                         draw_Picture(iconPos, icon, iconSize, '1 1 1', 1);
1211                 else
1212                         draw_Picture(iconPos, icon, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1213         }
1214
1215         iconPos.x += iconSize.x;
1216
1217         // Stats
1218         if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1219                 draw_Picture(iconPos, "icon_stats1", iconSize, '1 1 1', 1);
1220
1221         // --------------
1222         //  RENDER TEXT
1223         // --------------
1224
1225         // ping
1226         s = ftos(p);
1227         draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1228
1229         // server name
1230         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1231         draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1232
1233         // server map
1234         s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1235         draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1236
1237         // server gametype
1238         s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1239         draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1240
1241         // server playercount
1242         s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1243         draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1244 }
1245
1246 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1247 {
1248         vector org, sz;
1249
1250         org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1251         sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1252
1253         if(scan == K_ENTER || scan == K_KP_ENTER)
1254         {
1255                 ServerList_Connect_Click(NULL, me);
1256                 return true;
1257         }
1258         else if(scan == K_MOUSE2 || scan == K_SPACE)
1259         {
1260                 if(me.nItems != 0)
1261                 {
1262                         m_play_click_sound(MENU_SOUND_OPEN);
1263                         main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1264                         DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1265                         return true;
1266                 }
1267                 return false;
1268         }
1269         else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1270         {
1271                 if(me.nItems != 0)
1272                 {
1273                         me.toggleFavorite(me, me.selectedServer);
1274                         me.ipAddressBoxFocused = -1;
1275                         return true;
1276                 }
1277                 return false;
1278         }
1279         else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1280                 return true;
1281         else if(!me.controlledTextbox)
1282                 return false;
1283         else
1284                 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1285 }
1286
1287 float XonoticServerList_getTotalHeight(entity me)
1288 {
1289         float num_normal_rows = me.nItems;
1290         int num_headers = category_draw_count;
1291         return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1292 }
1293 int XonoticServerList_getItemAtPos(entity me, float pos)
1294 {
1295         pos = pos / me.itemHeight;
1296         int i;
1297         for (i = category_draw_count - 1; i >= 0; --i) {
1298                 int itemidx = category_item[i];
1299                 float itempos = i * me.categoriesHeight + category_item[i];
1300                 if (pos >= itempos + me.categoriesHeight + 1)
1301                         return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1302                 if (pos >= itempos)
1303                         return itemidx;
1304         }
1305         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1306         return floor(pos);
1307 }
1308 float XonoticServerList_getItemStart(entity me, int item)
1309 {
1310         int i;
1311         for (i = category_draw_count - 1; i >= 0; --i) {
1312                 int itemidx = category_item[i];
1313                 float itempos = i * me.categoriesHeight + category_item[i];
1314                 if (item >= itemidx + 1)
1315                         return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1316                 if (item >= itemidx)
1317                         return itempos * me.itemHeight;
1318         }
1319         // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1320         return item * me.itemHeight;
1321 }
1322 float XonoticServerList_getItemHeight(entity me, int item)
1323 {
1324         int i;
1325         for (i = 0; i < category_draw_count; ++i) {
1326                 // Matches exactly the headings with increased height.
1327                 if (item == category_item[i])
1328                         return me.itemHeight * (me.categoriesHeight + 1);
1329         }
1330         return me.itemHeight;
1331 }
1332
1333 #endif