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))
12 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
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)
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)
56 ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
58 ATTRIB(XonoticServerList, seenIPv4, float, 0)
59 ATTRIB(XonoticServerList, seenIPv6, float, 0)
60 ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
62 METHOD(XonoticServerList, getTotalHeight, float(entity))
63 METHOD(XonoticServerList, getItemAtPos, float(entity, float))
64 METHOD(XonoticServerList, getItemStart, float(entity, float))
65 METHOD(XonoticServerList, getItemHeight, float(entity, float))
66 ENDCLASS(XonoticServerList)
67 entity makeXonoticServerList();
69 #ifndef IMPLEMENTATION
70 var float autocvar_menu_slist_categories = TRUE;
71 var float autocvar_menu_slist_categories_onlyifmultiple = TRUE;
72 var float autocvar_menu_slist_purethreshold = 10;
73 var float autocvar_menu_slist_modimpurity = 10;
74 var float autocvar_menu_slist_recommendations = 3;
75 var float autocvar_menu_slist_recommendations_maxping = 150;
76 var float autocvar_menu_slist_recommendations_minfreeslots = 1;
77 var float autocvar_menu_slist_recommendations_minhumans = 0;
78 var float autocvar_menu_slist_recommendations_purethreshold = -1;
79 //var string autocvar_menu_slist_recommended = "76.124.107.5:26004";
81 // server cache fields
82 #define SLIST_FIELDS \
83 SLIST_FIELD(CNAME, "cname") \
84 SLIST_FIELD(PING, "ping") \
85 SLIST_FIELD(GAME, "game") \
86 SLIST_FIELD(MOD, "mod") \
87 SLIST_FIELD(MAP, "map") \
88 SLIST_FIELD(NAME, "name") \
89 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
90 SLIST_FIELD(NUMPLAYERS, "numplayers") \
91 SLIST_FIELD(NUMHUMANS, "numhumans") \
92 SLIST_FIELD(NUMBOTS, "numbots") \
93 SLIST_FIELD(PROTOCOL, "protocol") \
94 SLIST_FIELD(FREESLOTS, "freeslots") \
95 SLIST_FIELD(PLAYERS, "players") \
96 SLIST_FIELD(QCSTATUS, "qcstatus") \
97 SLIST_FIELD(CATEGORY, "category") \
98 SLIST_FIELD(ISFAVORITE, "isfavorite")
100 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
104 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
105 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
106 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
107 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
109 // function declarations
110 entity RetrieveCategoryEnt(float catnum);
112 float IsServerInList(string list, string srv);
113 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
114 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
116 float CheckCategoryOverride(float cat);
117 float CheckCategoryForEntry(float entry);
118 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
120 void RegisterSLCategories();
122 void ServerList_Connect_Click(entity btn, entity me);
123 void ServerList_Categories_Click(entity box, entity me);
124 void ServerList_ShowEmpty_Click(entity box, entity me);
125 void ServerList_ShowFull_Click(entity box, entity me);
126 void ServerList_Filter_Change(entity box, entity me);
127 void ServerList_Favorite_Click(entity btn, entity me);
128 void ServerList_Info_Click(entity btn, entity me);
129 void ServerList_Update_favoriteButton(entity btn, entity me);
131 // fields for category entities
132 #define MAX_CATEGORIES 9
133 #define CATEGORY_FIRST 1
134 entity categories[MAX_CATEGORIES];
135 float category_ent_count;
138 .string cat_enoverride_string;
139 .string cat_dioverride_string;
140 .float cat_enoverride;
141 .float cat_dioverride;
143 // fields for drawing categories
144 float category_name[MAX_CATEGORIES];
145 float category_item[MAX_CATEGORIES];
146 float category_draw_count;
148 #define SLIST_CATEGORIES \
149 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
150 SLIST_CATEGORY(CAT_RECOMMENDED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Recommended"))) \
151 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
152 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
153 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
154 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
155 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
156 SLIST_CATEGORY(CAT_MINSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^MinstaGib Mode"))) \
157 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
159 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
160 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
162 var string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
164 #undef SLIST_CATEGORY
168 #ifdef IMPLEMENTATION
170 void RegisterSLCategories()
173 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
174 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
175 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
177 categories[name - 1] = cat; \
178 cat.classname = "slist_category"; \
179 cat.cat_name = strzone(#name); \
180 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
181 cat.cat_dioverride_string = strzone(dioverride); \
182 cat.cat_string = strzone(str);
184 #undef SLIST_CATEGORY
189 #define PROCESS_OVERRIDE(override_string,override_field) \
190 for(i = 0; i < category_ent_count; ++i) \
192 s = categories[i].override_string; \
193 if((s != "") && (s != categories[i].cat_name)) \
196 for(x = 0; x < category_ent_count; ++x) \
197 { if(categories[x].cat_name == s) { \
203 strunzone(categories[i].override_string); \
204 categories[i].override_field = catnum; \
210 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
212 categories[i].cat_name \
216 strunzone(categories[i].override_string); \
217 categories[i].override_field = 0; \
219 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
220 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
221 #undef PROCESS_OVERRIDE
224 // Supporting Functions
225 entity RetrieveCategoryEnt(float catnum)
227 if((catnum > 0) && (catnum <= category_ent_count))
229 return categories[catnum - 1];
233 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
238 float IsServerInList(string list, string srv)
244 srv = netaddress_resolve(srv, 26000);
247 p = crypto_getidfp(srv);
248 n = tokenize_console(list);
249 for(i = 0; i < n; ++i)
251 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
259 if(srv == netaddress_resolve(argv(i), 26000))
266 float CheckCategoryOverride(float cat)
268 entity catent = RetrieveCategoryEnt(cat);
271 float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
272 if(override) { return override; }
277 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
282 float CheckCategoryForEntry(float entry)
284 string s, k, v, modtype = "";
285 float j, m, impure = 0, freeslots = 0, sflags = 0;
286 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
287 m = tokenizebyseparator(s, ":");
289 for(j = 2; j < m; ++j)
291 if(argv(j) == "") { break; }
292 k = substring(argv(j), 0, 1);
293 v = substring(argv(j), 1, -1);
296 case "P": { impure = stof(v); break; }
297 case "S": { freeslots = stof(v); break; }
298 case "F": { sflags = stof(v); break; }
299 case "M": { modtype = strtolower(v); break; }
303 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
305 // check if this server is favorited
306 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
308 // now check if it's recommended
309 if(autocvar_menu_slist_recommendations)
311 float recommended = 0;
312 if(autocvar_menu_slist_recommendations & 1)
314 if(IsRecommended(gethostcachestring(SLIST_FIELD_CNAME, entry)))
319 if(autocvar_menu_slist_recommendations & 2)
322 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
325 (autocvar_menu_slist_recommendations_purethreshold < 0)
327 (impure <= autocvar_menu_slist_recommendations_purethreshold)
331 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
333 autocvar_menu_slist_recommendations_minhumans
337 gethostcachenumber(SLIST_FIELD_PING, entry)
339 autocvar_menu_slist_recommendations_maxping
346 if(recommended > 0) { return CAT_RECOMMENDED; }
349 // if not favorited or recommended, check modname
350 if(modtype != "xonotic")
354 // old servers which don't report their mod name are considered modified now
355 case "": { return CAT_MODIFIED; }
357 case "xpm": { return CAT_XPM; }
358 case "minstagib": { return CAT_MINSTAGIB; }
359 case "overkill": { return CAT_OVERKILL; }
360 //case "nix": { return CAT_NIX; }
361 //case "newtoys": { return CAT_NEWTOYS; }
363 // "cts" is allowed as compat, xdf is replacement
365 case "xdf": { return CAT_DEFRAG; }
367 default: { dprint(sprintf("Found strange mod type: %s\n", modtype)); return CAT_MODIFIED; }
371 // must be normal or impure server
372 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
375 void XonoticServerList_toggleFavorite(entity me, string srv)
377 string s, s0, s1, s2, srv_resolved, p;
379 srv_resolved = netaddress_resolve(srv, 26000);
380 p = crypto_getidfp(srv_resolved);
381 s = cvar_string("net_slist_favorites");
382 n = tokenize_console(s);
384 for(i = 0; i < n; ++i)
386 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
394 if(srv_resolved != netaddress_resolve(argv(i), 26000))
399 s0 = substring(s, 0, argv_end_index(i - 1));
401 s2 = substring(s, argv_start_index(i + 1), -1);
402 if(s0 != "" && s2 != "")
404 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
405 s = cvar_string("net_slist_favorites");
406 n = tokenize_console(s);
417 cvar_set("net_slist_favorites", strcat(s, s1, p));
419 cvar_set("net_slist_favorites", strcat(s, s1, srv));
422 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
425 void ServerList_Update_favoriteButton(entity btn, entity me)
427 me.favoriteButton.setText(me.favoriteButton,
428 (IsFavorite(me.ipAddressBox.text) ?
429 _("Remove") : _("Favorite")
434 entity makeXonoticServerList()
437 me = spawnXonoticServerList();
438 me.configureXonoticServerList(me);
441 void XonoticServerList_configureXonoticServerList(entity me)
443 me.configureXonoticListBox(me);
446 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
453 void XonoticServerList_setSelected(entity me, float i)
456 save = me.selectedItem;
457 SUPER(XonoticServerList).setSelected(me, i);
459 if(me.selectedItem == save)
464 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
465 return; // sorry, it would be wrong
467 if(me.selectedServer)
468 strunzone(me.selectedServer);
469 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
471 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
472 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
473 me.ipAddressBoxFocused = -1;
475 void XonoticServerList_refreshServerList(entity me, float mode)
477 //print("refresh of type ", ftos(mode), "\n");
479 if(mode >= REFRESHSERVERLIST_REFILTER)
483 string s, typestr, modstr;
487 m = strstrofs(s, ":", 0);
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);
498 modstr = cvar_string("menu_slist_modfilter");
500 m = SLIST_MASK_AND - 1;
501 resethostcachemasks();
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);
507 if(!me.filterShowFull)
509 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
510 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
514 if(!me.filterShowEmpty)
515 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
517 // gametype filtering
519 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
524 if(substring(modstr, 0, 1) == "!")
525 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
527 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
531 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
532 for(i = 0; i < n; ++i)
534 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
536 m = SLIST_MASK_OR - 1;
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);
546 //listflags |= SLSF_FAVORITES;
547 listflags |= SLSF_CATEGORIES;
548 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
549 sethostcachesort(me.currentSortField, listflags);
553 if(mode >= REFRESHSERVERLIST_ASK)
554 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
556 void XonoticServerList_focusEnter(entity me)
558 if(time < me.nextRefreshTime)
560 //print("sorry, no refresh yet\n");
563 me.nextRefreshTime = time + 10;
564 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
567 void XonoticServerList_draw(entity me)
569 float i, found, owned;
571 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
575 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
578 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
582 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
585 if(me.currentSortField == -1)
587 me.setSortOrder(me, SLIST_FIELD_PING, +1);
588 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
590 else if(me.needsRefresh == 1)
592 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
594 else if(me.needsRefresh == 2)
597 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
599 else if(me.needsRefresh == 3)
602 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
605 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
607 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
608 category_draw_count = 0;
610 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
612 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
613 me.nItems = itemcount;
615 //float visible = floor(me.scrollPos / me.itemHeight);
616 // ^ unfortunately no such optimization can be made-- we must process through the
617 // entire list, otherwise there is no way to know which item is first in its category.
619 // binary search method suggested by div
622 for(x = 1; x <= category_ent_count; ++x) {
624 float last = (itemcount - 1);
629 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
630 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
632 // The first one is already > x.
633 // Therefore, category x does not exist.
634 // Higher numbered categories do exist though.
635 } else if (catl < x) {
636 // The last one is < x.
637 // Thus this category - and any following -
640 } else if (catf == x) {
641 // Starts at first. This breaks the loop
642 // invariant in the binary search and thus has
643 // to be handled separately.
644 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
645 error("Category mismatch I");
647 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
648 error("Category mismatch II");
649 category_name[category_draw_count] = x;
650 category_item[category_draw_count] = first;
651 ++category_draw_count;
654 // At this point, catf <= x < catl, thus
655 // catf < catl, thus first < last.
658 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
659 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
662 while (last - first > 1) {
663 float middle = floor((first + last) / 2);
664 // By loop condition, middle != first && middle != last.
665 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
675 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
676 error("Category mismatch III");
678 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
679 error("Category mismatch IV");
680 category_name[category_draw_count] = x;
681 category_item[category_draw_count] = last;
682 ++category_draw_count;
683 begin = last + 1; // already scanned through these, skip 'em
686 begin = last; // already scanned through these, skip 'em
689 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
691 category_name[0] = -1;
692 category_item[0] = -1;
693 category_draw_count = 0;
694 me.nItems = itemcount;
697 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
699 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
700 me.infoButton.disabled = ((me.nItems == 0) || !owned);
701 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
704 if(me.selectedServer)
706 for(i = 0; i < me.nItems; ++i)
708 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
710 if(i != me.selectedItem)
712 me.lastClickedServer = -1;
724 if(me.selectedItem >= me.nItems)
725 me.selectedItem = me.nItems - 1;
726 if(me.selectedServer)
727 strunzone(me.selectedServer);
728 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
734 if(me.selectedServer != me.ipAddressBox.text)
736 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
737 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
738 me.ipAddressBoxFocused = -1;
742 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
744 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
745 ServerList_Update_favoriteButton(NULL, me);
746 me.ipAddressBoxFocused = me.ipAddressBox.focused;
749 SUPER(XonoticServerList).draw(me);
751 void ServerList_PingSort_Click(entity btn, entity me)
753 me.setSortOrder(me, SLIST_FIELD_PING, +1);
755 void ServerList_NameSort_Click(entity btn, entity me)
757 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
759 void ServerList_MapSort_Click(entity btn, entity me)
761 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
763 void ServerList_PlayerSort_Click(entity btn, entity me)
765 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
767 void ServerList_TypeSort_Click(entity btn, entity me)
772 m = strstrofs(s, ":", 0);
775 s = substring(s, 0, m);
776 while(substring(s, m+1, 1) == " ") // skip spaces
782 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
784 t = MapInfo_Type_ToString(i);
786 if(t == "") // it repeats (default case)
789 // choose the first one
790 s = MapInfo_Type_ToString(1);
795 // the type was found
796 // choose the next one
797 s = MapInfo_Type_ToString(i * 2);
799 s = MapInfo_Type_ToString(1);
806 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
808 me.controlledTextbox.setText(me.controlledTextbox, s);
809 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
810 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
811 //ServerList_Filter_Change(me.controlledTextbox, me);
813 void ServerList_Filter_Change(entity box, entity me)
816 strunzone(me.filterString);
818 me.filterString = strzone(box.text);
820 me.filterString = string_null;
821 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
823 me.ipAddressBox.setText(me.ipAddressBox, "");
824 me.ipAddressBox.cursorPos = 0;
825 me.ipAddressBoxFocused = -1;
827 void ServerList_Categories_Click(entity box, entity me)
829 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
830 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
832 me.ipAddressBox.setText(me.ipAddressBox, "");
833 me.ipAddressBox.cursorPos = 0;
834 me.ipAddressBoxFocused = -1;
836 void ServerList_ShowEmpty_Click(entity box, entity me)
838 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
839 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
841 me.ipAddressBox.setText(me.ipAddressBox, "");
842 me.ipAddressBox.cursorPos = 0;
843 me.ipAddressBoxFocused = -1;
845 void ServerList_ShowFull_Click(entity box, entity me)
847 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
848 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
850 me.ipAddressBox.setText(me.ipAddressBox, "");
851 me.ipAddressBox.cursorPos = 0;
852 me.ipAddressBoxFocused = -1;
854 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
856 if(me.currentSortField == fld)
857 direction = -me.currentSortOrder;
858 me.currentSortOrder = direction;
859 me.currentSortField = fld;
860 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
861 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
862 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
863 me.sortButton4.forcePressed = 0;
864 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
866 if(me.selectedServer)
867 strunzone(me.selectedServer);
868 me.selectedServer = string_null;
869 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
871 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
873 vector originInLBSpace, sizeInLBSpace;
874 originInLBSpace = eY * (-me.itemHeight);
875 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
877 vector originInDialogSpace, sizeInDialogSpace;
878 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
879 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
881 btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
882 btn.Container_size_x = sizeInDialogSpace_x * theSize;
883 btn.setText(btn, theTitle);
884 btn.onClick = theFunc;
885 btn.onClickEntity = me;
888 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
890 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
892 me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
893 me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
894 me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
896 me.columnIconsOrigin = 0;
897 me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
898 me.columnPingSize = me.realFontSize_x * 3;
899 me.columnMapSize = me.realFontSize_x * 10;
900 me.columnTypeSize = me.realFontSize_x * 4;
901 me.columnPlayersSize = me.realFontSize_x * 5;
902 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
903 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
904 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
905 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
906 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
907 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
909 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
910 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
911 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
912 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
913 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
916 f = me.currentSortField;
919 me.currentSortField = -1;
920 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
923 void ServerList_Connect_Click(entity btn, entity me)
925 localcmd(sprintf("connect %s\n",
926 ((me.ipAddressBox.text != "") ?
927 me.ipAddressBox.text : me.selectedServer
931 void ServerList_Favorite_Click(entity btn, entity me)
934 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
937 me.toggleFavorite(me, me.ipAddressBox.text);
938 me.ipAddressBoxFocused = -1;
941 void ServerList_Info_Click(entity btn, entity me)
944 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
945 DialogOpenButton_Click(me, main.serverInfoDialog);
947 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
949 if(i == me.lastClickedServer)
950 if(time < me.lastClickedTime + 0.3)
953 ServerList_Connect_Click(NULL, me);
955 me.lastClickedServer = i;
956 me.lastClickedTime = time;
958 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
960 // layout: Ping, Server name, Map name, NP, TP, MP
965 float m, pure, freeslots, j, sflags;
966 string s, typestr, versionstr, k, v, modname;
968 //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
970 vector oldscale = draw_scale;
971 vector oldshift = draw_shift;
972 #define SET_YRANGE(start,end) \
973 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
974 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
976 for (j = 0; j < category_draw_count; ++j) {
977 // Matches exactly the headings with increased height.
978 if (i == category_item[j])
982 if (j < category_draw_count)
984 entity catent = RetrieveCategoryEnt(category_name[j]);
988 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
989 me.categoriesHeight / (me.categoriesHeight + 1)
992 eY * me.realUpperMargin
995 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
998 eX * (me.columnNameOrigin),
999 strcat(catent.cat_string, ":"),
1006 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1011 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1013 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1014 m = tokenizebyseparator(s, ":");
1019 versionstr = argv(1);
1025 for(j = 2; j < m; ++j)
1029 k = substring(argv(j), 0, 1);
1030 v = substring(argv(j), 1, -1);
1034 freeslots = stof(v);
1041 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1043 modname = "Xonotic";
1047 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1048 s = gethostcachestring(SLIST_FIELD_MOD, i);
1050 if(modname == "Xonotic")
1054 // list the mods here on which the pure server check actually works
1055 if(modname != "Xonotic")
1056 if(modname != "MinstaGib")
1057 if(modname != "CTS")
1058 if(modname != "NIX")
1059 if(modname != "NewToys")
1062 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1063 theAlpha = SKINALPHA_SERVERLIST_FULL;
1064 else if(freeslots == 0)
1065 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1066 else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1067 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1071 p = gethostcachenumber(SLIST_FIELD_PING, i);
1073 #define PING_MED 200
1074 #define PING_HIGH 500
1076 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1077 else if(p < PING_MED)
1078 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1079 else if(p < PING_HIGH)
1081 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1082 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1087 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1090 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1092 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1093 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1096 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1099 if(substring(s, 0, 1) == "[")
1104 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1110 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1111 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1113 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1114 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1119 if(cvar("crypto_aeslevel") >= 2)
1124 if(cvar("crypto_aeslevel") >= 1)
1134 // 2: AES recommended but not available
1135 // 3: AES possible and will be used
1136 // 4: AES recommended and will be used
1142 vector iconSize = '0 0 0';
1143 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1144 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1146 vector iconPos = '0 0 0';
1147 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1148 iconPos_y = (1 - iconSize_y) * 0.5;
1152 if not(me.seenIPv4 && me.seenIPv6)
1154 iconPos_x += iconSize_x * 0.5;
1156 else if(me.seenIPv4 && me.seenIPv6)
1160 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1162 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1164 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1165 iconPos_x += iconSize_x;
1170 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1171 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1173 iconPos_x += iconSize_x;
1175 if(modname == "Xonotic")
1179 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1180 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1185 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1186 if(draw_PictureSize(n) == '0 0 0')
1187 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1189 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1191 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1193 iconPos_x += iconSize_x;
1195 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1197 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1198 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1200 iconPos_x += iconSize_x;
1208 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1211 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1212 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1215 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1216 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1219 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1220 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1222 // server playercount
1223 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1224 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1227 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1229 float i = me.selectedItem;
1232 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1233 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1235 me.lastBumpSelectTime = 0;
1237 if(scan == K_ENTER || scan == K_KP_ENTER)
1239 ServerList_Connect_Click(NULL, me);
1242 else if(scan == K_MOUSE2 || scan == K_SPACE)
1246 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1247 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1252 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1256 me.toggleFavorite(me, me.selectedServer);
1257 me.ipAddressBoxFocused = -1;
1262 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1264 else if(!me.controlledTextbox)
1267 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1270 float XonoticServerList_getTotalHeight(entity me) {
1271 float num_normal_rows = me.nItems;
1272 float num_headers = category_draw_count;
1273 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1275 float XonoticServerList_getItemAtPos(entity me, float pos) {
1276 pos = pos / me.itemHeight;
1278 for (i = category_draw_count - 1; i >= 0; --i) {
1279 float itemidx = category_item[i];
1280 float itempos = i * me.categoriesHeight + category_item[i];
1281 if (pos >= itempos + me.categoriesHeight + 1)
1282 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1286 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1289 float XonoticServerList_getItemStart(entity me, float item) {
1291 for (i = category_draw_count - 1; i >= 0; --i) {
1292 float itemidx = category_item[i];
1293 float itempos = i * me.categoriesHeight + category_item[i];
1294 if (item >= itemidx + 1)
1295 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1296 if (item >= itemidx)
1297 return itempos * me.itemHeight;
1299 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1300 return item * me.itemHeight;
1302 float XonoticServerList_getItemHeight(entity me, float item) {
1304 for (i = 0; i < category_draw_count; ++i) {
1305 // Matches exactly the headings with increased height.
1306 if (item == category_item[i])
1307 return me.itemHeight * (me.categoriesHeight + 1);
1309 return me.itemHeight;