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, doubleClickListBoxItem, 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)
53 ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
55 ATTRIB(XonoticServerList, seenIPv4, float, 0)
56 ATTRIB(XonoticServerList, seenIPv6, float, 0)
57 ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
59 METHOD(XonoticServerList, getTotalHeight, float(entity))
60 METHOD(XonoticServerList, getItemAtPos, float(entity, float))
61 METHOD(XonoticServerList, getItemStart, float(entity, float))
62 METHOD(XonoticServerList, getItemHeight, float(entity, float))
63 ENDCLASS(XonoticServerList)
64 entity makeXonoticServerList();
66 #ifndef IMPLEMENTATION
67 float autocvar_menu_slist_categories;
68 float autocvar_menu_slist_categories_onlyifmultiple;
69 float autocvar_menu_slist_purethreshold;
70 float autocvar_menu_slist_modimpurity;
71 float autocvar_menu_slist_recommendations;
72 float autocvar_menu_slist_recommendations_maxping;
73 float autocvar_menu_slist_recommendations_minfreeslots;
74 float autocvar_menu_slist_recommendations_minhumans;
75 float autocvar_menu_slist_recommendations_purethreshold;
77 // server cache fields
78 #define SLIST_FIELDS \
79 SLIST_FIELD(CNAME, "cname") \
80 SLIST_FIELD(PING, "ping") \
81 SLIST_FIELD(GAME, "game") \
82 SLIST_FIELD(MOD, "mod") \
83 SLIST_FIELD(MAP, "map") \
84 SLIST_FIELD(NAME, "name") \
85 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
86 SLIST_FIELD(NUMPLAYERS, "numplayers") \
87 SLIST_FIELD(NUMHUMANS, "numhumans") \
88 SLIST_FIELD(NUMBOTS, "numbots") \
89 SLIST_FIELD(PROTOCOL, "protocol") \
90 SLIST_FIELD(FREESLOTS, "freeslots") \
91 SLIST_FIELD(PLAYERS, "players") \
92 SLIST_FIELD(QCSTATUS, "qcstatus") \
93 SLIST_FIELD(CATEGORY, "category") \
94 SLIST_FIELD(ISFAVORITE, "isfavorite")
96 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
100 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
101 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
102 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
103 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
105 // function declarations
106 float IsServerInList(string list, string srv);
107 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
108 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
109 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
111 entity RetrieveCategoryEnt(float catnum);
113 float CheckCategoryOverride(float cat);
114 float CheckCategoryForEntry(float entry);
115 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
117 void RegisterSLCategories();
119 void ServerList_Connect_Click(entity btn, entity me);
120 void ServerList_Categories_Click(entity box, entity me);
121 void ServerList_ShowEmpty_Click(entity box, entity me);
122 void ServerList_ShowFull_Click(entity box, entity me);
123 void ServerList_Filter_Change(entity box, entity me);
124 void ServerList_Favorite_Click(entity btn, entity me);
125 void ServerList_Info_Click(entity btn, entity me);
126 void ServerList_Update_favoriteButton(entity btn, entity me);
128 // fields for category entities
129 const float MAX_CATEGORIES = 9;
130 const float CATEGORY_FIRST = 1;
131 entity categories[MAX_CATEGORIES];
132 float category_ent_count;
135 .string cat_enoverride_string;
136 .string cat_dioverride_string;
137 .float cat_enoverride;
138 .float cat_dioverride;
140 // fields for drawing categories
141 float category_name[MAX_CATEGORIES];
142 float category_item[MAX_CATEGORIES];
143 float category_draw_count;
145 #define SLIST_CATEGORIES \
146 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
147 SLIST_CATEGORY(CAT_RECOMMENDED, "", "", ZCTX(_("SLCAT^Recommended"))) \
148 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
149 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
150 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
151 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
152 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
153 SLIST_CATEGORY(CAT_INSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^InstaGib Mode"))) \
154 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
156 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
157 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
159 string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
161 #undef SLIST_CATEGORY
165 #ifdef IMPLEMENTATION
167 void RegisterSLCategories()
170 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
171 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
172 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
174 categories[name - 1] = cat; \
175 cat.classname = "slist_category"; \
176 cat.cat_name = strzone(#name); \
177 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
178 cat.cat_dioverride_string = strzone(dioverride); \
179 cat.cat_string = strzone(str);
181 #undef SLIST_CATEGORY
186 #define PROCESS_OVERRIDE(override_string,override_field) \
187 for(i = 0; i < category_ent_count; ++i) \
189 s = categories[i].override_string; \
190 if((s != "") && (s != categories[i].cat_name)) \
193 for(x = 0; x < category_ent_count; ++x) \
194 { if(categories[x].cat_name == s) { \
200 strunzone(categories[i].override_string); \
201 categories[i].override_field = catnum; \
207 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
209 categories[i].cat_name \
213 strunzone(categories[i].override_string); \
214 categories[i].override_field = 0; \
216 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
217 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
218 #undef PROCESS_OVERRIDE
221 // Supporting Functions
222 entity RetrieveCategoryEnt(float catnum)
224 if((catnum > 0) && (catnum <= category_ent_count))
226 return categories[catnum - 1];
230 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
235 float IsServerInList(string list, string srv)
241 srv = netaddress_resolve(srv, 26000);
244 p = crypto_getidfp(srv);
245 n = tokenize_console(list);
246 for(i = 0; i < n; ++i)
248 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
256 if(srv == netaddress_resolve(argv(i), 26000))
263 float CheckCategoryOverride(float cat)
265 entity catent = RetrieveCategoryEnt(cat);
268 float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
269 if(override) { return override; }
274 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
279 float CheckCategoryForEntry(float entry)
281 string s, k, v, modtype = "";
282 float j, m, impure = 0, freeslots = 0, sflags = 0;
283 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
284 m = tokenizebyseparator(s, ":");
286 for(j = 2; j < m; ++j)
288 if(argv(j) == "") { break; }
289 k = substring(argv(j), 0, 1);
290 v = substring(argv(j), 1, -1);
293 case "P": { impure = stof(v); break; }
294 case "S": { freeslots = stof(v); break; }
295 case "F": { sflags = stof(v); break; }
296 case "M": { modtype = strtolower(v); break; }
300 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
302 // check if this server is favorited
303 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
305 // now check if it's recommended
306 if(autocvar_menu_slist_recommendations)
308 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
310 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
313 float recommended = 0;
314 if(autocvar_menu_slist_recommendations & 1)
316 if(IsRecommended(cname)) { ++recommended; }
317 else { --recommended; }
319 if(autocvar_menu_slist_recommendations & 2)
322 ///// check for minimum free slots
323 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
325 && // check for purity requirement
327 (autocvar_menu_slist_recommendations_purethreshold < 0)
329 (impure <= autocvar_menu_slist_recommendations_purethreshold)
332 && // check for minimum amount of humans
334 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
336 autocvar_menu_slist_recommendations_minhumans
339 && // check for maximum latency
341 gethostcachenumber(SLIST_FIELD_PING, entry)
343 autocvar_menu_slist_recommendations_maxping
350 if(recommended > 0) { return CAT_RECOMMENDED; }
354 // if not favorited or recommended, check modname
355 if(modtype != "xonotic")
359 // old servers which don't report their mod name are considered modified now
360 case "": { return CAT_MODIFIED; }
362 case "xpm": { return CAT_XPM; }
364 case "instagib": { return CAT_INSTAGIB; }
365 case "overkill": { return CAT_OVERKILL; }
366 //case "nix": { return CAT_NIX; }
367 //case "newtoys": { return CAT_NEWTOYS; }
369 // "cts" is allowed as compat, xdf is replacement
371 case "xdf": { return CAT_DEFRAG; }
373 default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
377 // must be normal or impure server
378 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
381 void XonoticServerList_toggleFavorite(entity me, string srv)
383 string s, s0, s1, s2, srv_resolved, p;
385 srv_resolved = netaddress_resolve(srv, 26000);
386 p = crypto_getidfp(srv_resolved);
387 s = cvar_string("net_slist_favorites");
388 n = tokenize_console(s);
390 for(i = 0; i < n; ++i)
392 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
400 if(srv_resolved != netaddress_resolve(argv(i), 26000))
405 s0 = substring(s, 0, argv_end_index(i - 1));
407 s2 = substring(s, argv_start_index(i + 1), -1);
408 if(s0 != "" && s2 != "")
410 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
411 s = cvar_string("net_slist_favorites");
412 n = tokenize_console(s);
423 cvar_set("net_slist_favorites", strcat(s, s1, p));
425 cvar_set("net_slist_favorites", strcat(s, s1, srv));
428 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
431 void ServerList_Update_favoriteButton(entity btn, entity me)
433 me.favoriteButton.setText(me.favoriteButton,
434 (IsFavorite(me.ipAddressBox.text) ?
435 _("Remove") : _("Favorite")
440 entity makeXonoticServerList()
443 me = spawnXonoticServerList();
444 me.configureXonoticServerList(me);
447 void XonoticServerList_configureXonoticServerList(entity me)
449 me.configureXonoticListBox(me);
452 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
459 void XonoticServerList_setSelected(entity me, float i)
462 save = me.selectedItem;
463 SUPER(XonoticServerList).setSelected(me, i);
465 if(me.selectedItem == save)
470 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
471 return; // sorry, it would be wrong
473 if(me.selectedServer)
474 strunzone(me.selectedServer);
475 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
477 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
478 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
479 me.ipAddressBoxFocused = -1;
481 void XonoticServerList_refreshServerList(entity me, float mode)
483 //print("refresh of type ", ftos(mode), "\n");
485 if(mode >= REFRESHSERVERLIST_REFILTER)
489 string s, typestr, modstr;
493 m = strstrofs(s, ":", 0);
496 typestr = substring(s, 0, m);
497 s = substring(s, m + 1, strlen(s) - m - 1);
498 while(substring(s, 0, 1) == " ")
499 s = substring(s, 1, strlen(s) - 1);
504 modstr = cvar_string("menu_slist_modfilter");
506 m = SLIST_MASK_AND - 1;
507 resethostcachemasks();
509 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
510 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
513 if(!me.filterShowFull)
515 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
516 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
520 if(!me.filterShowEmpty)
521 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
523 // gametype filtering
525 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
530 if(substring(modstr, 0, 1) == "!")
531 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
533 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
537 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
538 for(i = 0; i < n; ++i)
540 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
542 m = SLIST_MASK_OR - 1;
545 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
546 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
547 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
548 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
552 //listflags |= SLSF_FAVORITES;
553 listflags |= SLSF_CATEGORIES;
554 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
555 sethostcachesort(me.currentSortField, listflags);
559 if(mode >= REFRESHSERVERLIST_ASK)
560 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
562 void XonoticServerList_focusEnter(entity me)
564 if(time < me.nextRefreshTime)
566 //print("sorry, no refresh yet\n");
569 me.nextRefreshTime = time + 10;
570 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
573 void XonoticServerList_draw(entity me)
575 float i, found, owned;
577 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
581 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
584 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
588 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
591 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
595 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
598 if(me.currentSortField == -1)
600 me.setSortOrder(me, SLIST_FIELD_PING, +1);
601 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
603 else if(me.needsRefresh == 1)
605 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
607 else if(me.needsRefresh == 2)
610 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
612 else if(me.needsRefresh == 3)
615 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
618 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
620 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
621 category_draw_count = 0;
623 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
625 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
626 me.nItems = itemcount;
628 //float visible = floor(me.scrollPos / me.itemHeight);
629 // ^ unfortunately no such optimization can be made-- we must process through the
630 // entire list, otherwise there is no way to know which item is first in its category.
632 // binary search method suggested by div
635 for(x = 1; x <= category_ent_count; ++x) {
637 float last = (itemcount - 1);
642 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
643 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
645 // The first one is already > x.
646 // Therefore, category x does not exist.
647 // Higher numbered categories do exist though.
648 } else if (catl < x) {
649 // The last one is < x.
650 // Thus this category - and any following -
653 } else if (catf == x) {
654 // Starts at first. This breaks the loop
655 // invariant in the binary search and thus has
656 // to be handled separately.
657 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
658 error("Category mismatch I");
660 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
661 error("Category mismatch II");
662 category_name[category_draw_count] = x;
663 category_item[category_draw_count] = first;
664 ++category_draw_count;
667 // At this point, catf <= x < catl, thus
668 // catf < catl, thus first < last.
671 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
672 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
675 while (last - first > 1) {
676 float middle = floor((first + last) / 2);
677 // By loop condition, middle != first && middle != last.
678 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
688 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
689 error("Category mismatch III");
691 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
692 error("Category mismatch IV");
693 category_name[category_draw_count] = x;
694 category_item[category_draw_count] = last;
695 ++category_draw_count;
696 begin = last + 1; // already scanned through these, skip 'em
699 begin = last; // already scanned through these, skip 'em
702 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
704 category_name[0] = -1;
705 category_item[0] = -1;
706 category_draw_count = 0;
707 me.nItems = itemcount;
710 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
712 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
713 me.infoButton.disabled = ((me.nItems == 0) || !owned);
714 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
717 if(me.selectedServer)
719 for(i = 0; i < me.nItems; ++i)
721 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
733 if(me.selectedItem >= me.nItems)
734 me.selectedItem = me.nItems - 1;
735 if(me.selectedServer)
736 strunzone(me.selectedServer);
737 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
743 if(me.selectedServer != me.ipAddressBox.text)
745 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
746 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
747 me.ipAddressBoxFocused = -1;
751 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
753 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
754 ServerList_Update_favoriteButton(NULL, me);
755 me.ipAddressBoxFocused = me.ipAddressBox.focused;
758 SUPER(XonoticServerList).draw(me);
760 void ServerList_PingSort_Click(entity btn, entity me)
762 me.setSortOrder(me, SLIST_FIELD_PING, +1);
764 void ServerList_NameSort_Click(entity btn, entity me)
766 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
768 void ServerList_MapSort_Click(entity btn, entity me)
770 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
772 void ServerList_PlayerSort_Click(entity btn, entity me)
774 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
776 void ServerList_TypeSort_Click(entity btn, entity me)
781 m = strstrofs(s, ":", 0);
784 s = substring(s, 0, m);
785 while(substring(s, m+1, 1) == " ") // skip spaces
791 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
793 t = MapInfo_Type_ToString(i);
795 if(t == "") // it repeats (default case)
798 // choose the first one
799 s = MapInfo_Type_ToString(1);
804 // the type was found
805 // choose the next one
806 s = MapInfo_Type_ToString(i * 2);
808 s = MapInfo_Type_ToString(1);
815 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
817 me.controlledTextbox.setText(me.controlledTextbox, s);
818 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
819 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
820 //ServerList_Filter_Change(me.controlledTextbox, me);
822 void ServerList_Filter_Change(entity box, entity me)
825 strunzone(me.filterString);
827 me.filterString = strzone(box.text);
829 me.filterString = string_null;
830 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
832 me.ipAddressBox.setText(me.ipAddressBox, "");
833 me.ipAddressBox.cursorPos = 0;
834 me.ipAddressBoxFocused = -1;
836 void ServerList_Categories_Click(entity box, entity me)
838 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
839 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
841 me.ipAddressBox.setText(me.ipAddressBox, "");
842 me.ipAddressBox.cursorPos = 0;
843 me.ipAddressBoxFocused = -1;
845 void ServerList_ShowEmpty_Click(entity box, entity me)
847 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
848 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
850 me.ipAddressBox.setText(me.ipAddressBox, "");
851 me.ipAddressBox.cursorPos = 0;
852 me.ipAddressBoxFocused = -1;
854 void ServerList_ShowFull_Click(entity box, entity me)
856 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
857 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
859 me.ipAddressBox.setText(me.ipAddressBox, "");
860 me.ipAddressBox.cursorPos = 0;
861 me.ipAddressBoxFocused = -1;
863 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
865 if(me.currentSortField == fld)
866 direction = -me.currentSortOrder;
867 me.currentSortOrder = direction;
868 me.currentSortField = fld;
869 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
870 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
871 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
872 me.sortButton4.forcePressed = 0;
873 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
875 if(me.selectedServer)
876 strunzone(me.selectedServer);
877 me.selectedServer = string_null;
878 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
880 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
882 vector originInLBSpace, sizeInLBSpace;
883 originInLBSpace = eY * (-me.itemHeight);
884 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
886 vector originInDialogSpace, sizeInDialogSpace;
887 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
888 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
890 btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
891 btn.Container_size_x = sizeInDialogSpace.x * theSize;
892 btn.setText(btn, theTitle);
893 btn.onClick = theFunc;
894 btn.onClickEntity = me;
897 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
899 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
901 me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
902 me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
903 me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
905 me.columnIconsOrigin = 0;
906 me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
907 me.columnPingSize = me.realFontSize.x * 3;
908 me.columnMapSize = me.realFontSize.x * 10;
909 me.columnTypeSize = me.realFontSize.x * 4;
910 me.columnPlayersSize = me.realFontSize.x * 5;
911 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
912 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
913 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
914 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
915 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
916 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
918 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
919 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
920 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
921 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
922 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
925 f = me.currentSortField;
928 me.currentSortField = -1;
929 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
932 void ServerList_Connect_Click(entity btn, entity me)
934 localcmd(sprintf("connect %s\n",
935 ((me.ipAddressBox.text != "") ?
936 me.ipAddressBox.text : me.selectedServer
940 void ServerList_Favorite_Click(entity btn, entity me)
943 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
946 me.toggleFavorite(me, me.ipAddressBox.text);
947 me.ipAddressBoxFocused = -1;
950 void ServerList_Info_Click(entity btn, entity me)
953 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
955 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
956 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
957 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
959 void XonoticServerList_doubleClickListBoxItem(entity me, float i, vector where)
961 ServerList_Connect_Click(NULL, me);
963 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
965 // layout: Ping, Server name, Map name, NP, TP, MP
970 float m, pure, freeslots, j, sflags;
971 string s, typestr, versionstr, k, v, modname;
973 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
975 vector oldscale = draw_scale;
976 vector oldshift = draw_shift;
977 #define SET_YRANGE(start,end) \
978 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
979 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
981 for (j = 0; j < category_draw_count; ++j) {
982 // Matches exactly the headings with increased height.
983 if (i == category_item[j])
987 if (j < category_draw_count)
989 entity catent = RetrieveCategoryEnt(category_name[j]);
993 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
994 me.categoriesHeight / (me.categoriesHeight + 1)
997 eY * me.realUpperMargin
1000 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1003 eX * (me.columnNameOrigin),
1004 strcat(catent.cat_string, ":"),
1007 SKINCOLOR_SERVERLIST_CATEGORY,
1008 SKINALPHA_SERVERLIST_CATEGORY,
1011 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1016 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1018 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1019 m = tokenizebyseparator(s, ":");
1024 versionstr = argv(1);
1030 for(j = 2; j < m; ++j)
1034 k = substring(argv(j), 0, 1);
1035 v = substring(argv(j), 1, -1);
1039 freeslots = stof(v);
1046 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1048 modname = "Xonotic";
1052 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1053 s = gethostcachestring(SLIST_FIELD_MOD, i);
1055 if(modname == "Xonotic")
1059 // list the mods here on which the pure server check actually works
1060 if(modname != "Xonotic")
1061 if(modname != "InstaGib" || modname != "MinstaGib")
1062 if(modname != "CTS")
1063 if(modname != "NIX")
1064 if(modname != "NewToys")
1067 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1068 theAlpha = SKINALPHA_SERVERLIST_FULL;
1069 else if(freeslots == 0)
1070 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1071 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1072 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1076 p = gethostcachenumber(SLIST_FIELD_PING, i);
1077 const float PING_LOW = 75;
1078 const float PING_MED = 200;
1079 const float PING_HIGH = 500;
1081 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1082 else if(p < PING_MED)
1083 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1084 else if(p < PING_HIGH)
1086 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1087 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1092 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1095 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1097 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1098 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1101 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1104 if(substring(s, 0, 1) == "[")
1109 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1115 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1116 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1118 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1119 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1124 if(cvar("crypto_aeslevel") >= 2)
1129 if(cvar("crypto_aeslevel") >= 1)
1139 // 2: AES recommended but not available
1140 // 3: AES possible and will be used
1141 // 4: AES recommended and will be used
1147 vector iconSize = '0 0 0';
1148 iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1149 iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1151 vector iconPos = '0 0 0';
1152 iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1153 iconPos_y = (1 - iconSize.y) * 0.5;
1157 if (!(me.seenIPv4 && me.seenIPv6))
1159 iconPos.x += iconSize.x * 0.5;
1161 else if(me.seenIPv4 && me.seenIPv6)
1165 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1167 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1169 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1170 iconPos.x += iconSize.x;
1175 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1176 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1178 iconPos.x += iconSize.x;
1180 if(modname == "Xonotic")
1184 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1185 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1190 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1191 if(draw_PictureSize(n) == '0 0 0')
1192 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1194 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1196 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1198 iconPos.x += iconSize.x;
1200 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1202 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1203 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1205 iconPos.x += iconSize.x;
1213 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1216 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1217 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1220 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1221 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1224 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1225 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1227 // server playercount
1228 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1229 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1232 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1236 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1237 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1239 if(scan == K_ENTER || scan == K_KP_ENTER)
1241 ServerList_Connect_Click(NULL, me);
1244 else if(scan == K_MOUSE2 || scan == K_SPACE)
1248 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1249 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1254 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1258 me.toggleFavorite(me, me.selectedServer);
1259 me.ipAddressBoxFocused = -1;
1264 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1266 else if(!me.controlledTextbox)
1269 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1272 float XonoticServerList_getTotalHeight(entity me) {
1273 float num_normal_rows = me.nItems;
1274 float num_headers = category_draw_count;
1275 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1277 float XonoticServerList_getItemAtPos(entity me, float pos) {
1278 pos = pos / me.itemHeight;
1280 for (i = category_draw_count - 1; i >= 0; --i) {
1281 float itemidx = category_item[i];
1282 float itempos = i * me.categoriesHeight + category_item[i];
1283 if (pos >= itempos + me.categoriesHeight + 1)
1284 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1288 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1291 float XonoticServerList_getItemStart(entity me, float item) {
1293 for (i = category_draw_count - 1; i >= 0; --i) {
1294 float itemidx = category_item[i];
1295 float itempos = i * me.categoriesHeight + category_item[i];
1296 if (item >= itemidx + 1)
1297 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1298 if (item >= itemidx)
1299 return itempos * me.itemHeight;
1301 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1302 return item * me.itemHeight;
1304 float XonoticServerList_getItemHeight(entity me, float item) {
1306 for (i = 0; i < category_draw_count; ++i) {
1307 // Matches exactly the headings with increased height.
1308 if (item == category_item[i])
1309 return me.itemHeight * (me.categoriesHeight + 1);
1311 return me.itemHeight;