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