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