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