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