4 CLASS(XonoticServerList, XonoticListBox)
5 METHOD(XonoticServerList, configureXonoticServerList, void(entity))
6 ATTRIB(XonoticServerList, rowsPerItem, float, 1)
7 METHOD(XonoticServerList, draw, void(entity))
8 METHOD(XonoticServerList, drawListBoxItem, void(entity, int, vector, bool, bool))
9 METHOD(XonoticServerList, doubleClickListBoxItem, void(entity, float, vector))
10 METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
11 METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
12 METHOD(XonoticServerList, toggleFavorite, void(entity, string))
14 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
16 ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
17 ATTRIB(XonoticServerList, realUpperMargin, float, 0)
18 ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
19 ATTRIB(XonoticServerList, columnIconsSize, float, 0)
20 ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
21 ATTRIB(XonoticServerList, columnPingSize, float, 0)
22 ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
23 ATTRIB(XonoticServerList, columnNameSize, float, 0)
24 ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
25 ATTRIB(XonoticServerList, columnMapSize, float, 0)
26 ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
27 ATTRIB(XonoticServerList, columnTypeSize, float, 0)
28 ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
29 ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
30 ATTRIB(XonoticServerList, lockedSelectedItem, bool, true) // initially keep selected the first item of the list, avoiding an unwanted scrolling
32 ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
33 METHOD(XonoticServerList, setSelected, void(entity, float))
34 METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
35 ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
36 ATTRIB(XonoticServerList, filterShowFull, float, 1)
37 ATTRIB(XonoticServerList, filterString, string, string_null)
38 ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
39 ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
40 ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
41 ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
42 METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
43 ATTRIB(XonoticServerList, needsRefresh, float, 1)
44 METHOD(XonoticServerList, focusEnter, void(entity))
45 METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
46 ATTRIB(XonoticServerList, sortButton1, entity, NULL)
47 ATTRIB(XonoticServerList, sortButton2, entity, NULL)
48 ATTRIB(XonoticServerList, sortButton3, entity, NULL)
49 ATTRIB(XonoticServerList, sortButton4, entity, NULL)
50 ATTRIB(XonoticServerList, sortButton5, entity, NULL)
51 ATTRIB(XonoticServerList, connectButton, entity, NULL)
52 ATTRIB(XonoticServerList, infoButton, entity, NULL)
53 ATTRIB(XonoticServerList, currentSortOrder, float, 0)
54 ATTRIB(XonoticServerList, currentSortField, float, -1)
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 float autocvar_menu_slist_categories;
71 float autocvar_menu_slist_categories_onlyifmultiple;
72 float autocvar_menu_slist_purethreshold;
73 float autocvar_menu_slist_modimpurity;
74 float autocvar_menu_slist_recommendations;
75 float autocvar_menu_slist_recommendations_maxping;
76 float autocvar_menu_slist_recommendations_minfreeslots;
77 float autocvar_menu_slist_recommendations_minhumans;
78 float autocvar_menu_slist_recommendations_purethreshold;
80 // server cache fields
81 #define SLIST_FIELDS \
82 SLIST_FIELD(CNAME, "cname") \
83 SLIST_FIELD(PING, "ping") \
84 SLIST_FIELD(GAME, "game") \
85 SLIST_FIELD(MOD, "mod") \
86 SLIST_FIELD(MAP, "map") \
87 SLIST_FIELD(NAME, "name") \
88 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
89 SLIST_FIELD(NUMPLAYERS, "numplayers") \
90 SLIST_FIELD(NUMHUMANS, "numhumans") \
91 SLIST_FIELD(NUMBOTS, "numbots") \
92 SLIST_FIELD(PROTOCOL, "protocol") \
93 SLIST_FIELD(FREESLOTS, "freeslots") \
94 SLIST_FIELD(PLAYERS, "players") \
95 SLIST_FIELD(QCSTATUS, "qcstatus") \
96 SLIST_FIELD(CATEGORY, "category") \
97 SLIST_FIELD(ISFAVORITE, "isfavorite")
99 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
103 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
104 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
105 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
106 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
108 // function declarations
109 float IsServerInList(string list, string srv);
110 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
111 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
112 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
114 entity RetrieveCategoryEnt(float catnum);
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 const int MAX_CATEGORIES = 9;
133 const int CATEGORY_FIRST = 1;
134 entity categories[MAX_CATEGORIES];
135 int 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 int category_name[MAX_CATEGORIES];
145 int category_item[MAX_CATEGORIES];
146 int category_draw_count;
148 #define SLIST_CATEGORIES \
149 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
150 SLIST_CATEGORY(CAT_RECOMMENDED, "", "", 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_INSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^InstaGib 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 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(int 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 bool 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 int CheckCategoryOverride(int cat)
268 entity catent = RetrieveCategoryEnt(cat);
271 int 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 int CheckCategoryForEntry(int entry)
284 string s, k, v, modtype = "";
285 int 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 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
313 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
316 float recommended = 0;
317 if(autocvar_menu_slist_recommendations & 1)
319 if(IsRecommended(cname)) { ++recommended; }
320 else { --recommended; }
322 if(autocvar_menu_slist_recommendations & 2)
325 ///// check for minimum free slots
326 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
328 && // check for purity requirement
330 (autocvar_menu_slist_recommendations_purethreshold < 0)
332 (impure <= autocvar_menu_slist_recommendations_purethreshold)
335 && // check for minimum amount of humans
337 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
339 autocvar_menu_slist_recommendations_minhumans
342 && // check for maximum latency
344 gethostcachenumber(SLIST_FIELD_PING, entry)
346 autocvar_menu_slist_recommendations_maxping
353 if(recommended > 0) { return CAT_RECOMMENDED; }
357 // if not favorited or recommended, check modname
358 if(modtype != "xonotic")
362 // old servers which don't report their mod name are considered modified now
363 case "": { return CAT_MODIFIED; }
365 case "xpm": { return CAT_XPM; }
367 case "instagib": { return CAT_INSTAGIB; }
368 case "overkill": { return CAT_OVERKILL; }
369 //case "nix": { return CAT_NIX; }
370 //case "newtoys": { return CAT_NEWTOYS; }
372 // "cts" is allowed as compat, xdf is replacement
374 case "xdf": { return CAT_DEFRAG; }
376 default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
380 // must be normal or impure server
381 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
384 void XonoticServerList_toggleFavorite(entity me, string srv)
386 string s, s0, s1, s2, srv_resolved, p;
389 srv_resolved = netaddress_resolve(srv, 26000);
390 p = crypto_getidfp(srv_resolved);
391 s = cvar_string("net_slist_favorites");
392 n = tokenize_console(s);
393 for(i = 0; i < n; ++i)
395 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
403 if(srv_resolved != netaddress_resolve(argv(i), 26000))
408 s0 = substring(s, 0, argv_end_index(i - 1));
410 s2 = substring(s, argv_start_index(i + 1), -1);
411 if(s0 != "" && s2 != "")
413 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
414 s = cvar_string("net_slist_favorites");
415 n = tokenize_console(s);
426 cvar_set("net_slist_favorites", strcat(s, s1, p));
428 cvar_set("net_slist_favorites", strcat(s, s1, srv));
431 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
434 void ServerList_Update_favoriteButton(entity btn, entity me)
436 me.favoriteButton.setText(me.favoriteButton,
437 (IsFavorite(me.ipAddressBox.text) ?
438 _("Remove") : _("Favorite")
443 entity makeXonoticServerList()
446 me = NEW(XonoticServerList);
447 me.configureXonoticServerList(me);
450 void XonoticServerList_configureXonoticServerList(entity me)
452 me.configureXonoticListBox(me);
455 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
462 void XonoticServerList_setSelected(entity me, int i)
464 me.lockedSelectedItem = false;
465 //int save = me.selectedItem;
466 SUPER(XonoticServerList).setSelected(me, i);
468 if(me.selectedItem == save)
473 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
474 return; // sorry, it would be wrong
476 if(me.selectedServer)
477 strunzone(me.selectedServer);
478 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
480 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
481 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
482 me.ipAddressBoxFocused = -1;
484 void XonoticServerList_refreshServerList(entity me, int mode)
486 //print("refresh of type ", ftos(mode), "\n");
488 if(mode >= REFRESHSERVERLIST_REFILTER)
493 string s, typestr, modstr;
497 m = strstrofs(s, ":", 0);
500 typestr = substring(s, 0, m);
501 s = substring(s, m + 1, strlen(s) - m - 1);
502 while(substring(s, 0, 1) == " ")
503 s = substring(s, 1, strlen(s) - 1);
508 modstr = cvar_string("menu_slist_modfilter");
510 m = SLIST_MASK_AND - 1;
511 resethostcachemasks();
513 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
514 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
517 if(!me.filterShowFull)
519 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
520 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
524 if(!me.filterShowEmpty)
525 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
527 // gametype filtering
529 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
534 if(substring(modstr, 0, 1) == "!")
535 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
537 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
541 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
542 for(i = 0; i < n; ++i)
544 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
546 m = SLIST_MASK_OR - 1;
549 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
550 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
551 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
552 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
556 //listflags |= SLSF_FAVORITES;
557 listflags |= SLSF_CATEGORIES;
558 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
559 sethostcachesort(me.currentSortField, listflags);
563 if(mode >= REFRESHSERVERLIST_ASK)
564 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
566 void XonoticServerList_focusEnter(entity me)
568 SUPER(XonoticServerList).focusEnter(me);
569 if(time < me.nextRefreshTime)
571 //print("sorry, no refresh yet\n");
574 me.nextRefreshTime = time + 10;
575 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
578 void XonoticServerList_draw(entity me)
581 bool found = false, owned;
583 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
587 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
590 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
594 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
597 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
601 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
604 if(me.currentSortField == -1)
606 me.setSortOrder(me, SLIST_FIELD_PING, +1);
607 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
609 else if(me.needsRefresh == 1)
611 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
613 else if(me.needsRefresh == 2)
616 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
618 else if(me.needsRefresh == 3)
621 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
624 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
626 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
627 category_draw_count = 0;
629 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
631 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
632 me.nItems = itemcount;
634 //float visible = floor(me.scrollPos / me.itemHeight);
635 // ^ unfortunately no such optimization can be made-- we must process through the
636 // entire list, otherwise there is no way to know which item is first in its category.
638 // binary search method suggested by div
641 for(x = 1; x <= category_ent_count; ++x) {
643 float last = (itemcount - 1);
648 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
649 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
651 // The first one is already > x.
652 // Therefore, category x does not exist.
653 // Higher numbered categories do exist though.
654 } else if (catl < x) {
655 // The last one is < x.
656 // Thus this category - and any following -
659 } else if (catf == x) {
660 // Starts at first. This breaks the loop
661 // invariant in the binary search and thus has
662 // to be handled separately.
663 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
664 error("Category mismatch I");
666 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
667 error("Category mismatch II");
668 category_name[category_draw_count] = x;
669 category_item[category_draw_count] = first;
670 ++category_draw_count;
673 // At this point, catf <= x < catl, thus
674 // catf < catl, thus first < last.
677 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
678 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
681 while (last - first > 1) {
682 float middle = floor((first + last) / 2);
683 // By loop condition, middle != first && middle != last.
684 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
694 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
695 error("Category mismatch III");
697 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
698 error("Category mismatch IV");
699 category_name[category_draw_count] = x;
700 category_item[category_draw_count] = last;
701 ++category_draw_count;
702 begin = last + 1; // already scanned through these, skip 'em
705 begin = last; // already scanned through these, skip 'em
708 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
710 category_name[0] = -1;
711 category_item[0] = -1;
712 category_draw_count = 0;
713 me.nItems = itemcount;
716 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
718 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
719 me.infoButton.disabled = ((me.nItems == 0) || !owned);
720 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
722 if(me.lockedSelectedItem)
726 if(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem) != me.selectedServer)
728 if(me.selectedServer)
729 strunzone(me.selectedServer);
730 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
735 else if(me.selectedServer)
737 for(i = 0; i < me.nItems; ++i)
739 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
741 // don't follow the selected item with SUPER(XonoticServerList).setSelected(me, i);
752 // selected server disappeared, select the last server (scrolling to it)
753 if(me.selectedItem >= me.nItems)
754 SUPER(XonoticServerList).setSelected(me, me.nItems - 1);
755 if(me.selectedServer)
756 strunzone(me.selectedServer);
757 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
763 if(me.selectedServer != me.ipAddressBox.text)
765 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
766 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
767 me.ipAddressBoxFocused = -1;
771 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
773 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
774 ServerList_Update_favoriteButton(NULL, me);
775 me.ipAddressBoxFocused = me.ipAddressBox.focused;
778 SUPER(XonoticServerList).draw(me);
780 void ServerList_PingSort_Click(entity btn, entity me)
782 me.setSortOrder(me, SLIST_FIELD_PING, +1);
784 void ServerList_NameSort_Click(entity btn, entity me)
786 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
788 void ServerList_MapSort_Click(entity btn, entity me)
790 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
792 void ServerList_PlayerSort_Click(entity btn, entity me)
794 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
796 void ServerList_TypeSort_Click(entity btn, entity me)
801 m = strstrofs(s, ":", 0);
804 s = substring(s, 0, m);
805 while(substring(s, m+1, 1) == " ") // skip spaces
811 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
813 t = MapInfo_Type_ToString(i);
815 if(t == "") // it repeats (default case)
818 // choose the first one
819 s = MapInfo_Type_ToString(1);
824 // the type was found
825 // choose the next one
826 s = MapInfo_Type_ToString(i * 2);
828 s = MapInfo_Type_ToString(1);
835 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
837 me.controlledTextbox.setText(me.controlledTextbox, s);
838 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
839 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
840 //ServerList_Filter_Change(me.controlledTextbox, me);
842 void ServerList_Filter_Change(entity box, entity me)
845 strunzone(me.filterString);
847 me.filterString = strzone(box.text);
849 me.filterString = string_null;
850 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
852 me.ipAddressBox.setText(me.ipAddressBox, "");
853 me.ipAddressBox.cursorPos = 0;
854 me.ipAddressBoxFocused = -1;
856 void ServerList_Categories_Click(entity box, entity me)
858 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
859 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
861 me.ipAddressBox.setText(me.ipAddressBox, "");
862 me.ipAddressBox.cursorPos = 0;
863 me.ipAddressBoxFocused = -1;
865 void ServerList_ShowEmpty_Click(entity box, entity me)
867 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
868 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
870 me.ipAddressBox.setText(me.ipAddressBox, "");
871 me.ipAddressBox.cursorPos = 0;
872 me.ipAddressBoxFocused = -1;
874 void ServerList_ShowFull_Click(entity box, entity me)
876 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
877 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
879 me.ipAddressBox.setText(me.ipAddressBox, "");
880 me.ipAddressBox.cursorPos = 0;
881 me.ipAddressBoxFocused = -1;
883 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
885 if(me.currentSortField == fld)
886 direction = -me.currentSortOrder;
887 me.currentSortOrder = direction;
888 me.currentSortField = fld;
889 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
890 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
891 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
892 me.sortButton4.forcePressed = 0;
893 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
895 if(me.selectedServer)
896 strunzone(me.selectedServer);
897 me.selectedServer = string_null;
898 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
900 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
902 vector originInLBSpace, sizeInLBSpace;
903 originInLBSpace = eY * (-me.itemHeight);
904 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
906 vector originInDialogSpace, sizeInDialogSpace;
907 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
908 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
910 btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
911 btn.Container_size_x = sizeInDialogSpace.x * theSize;
912 btn.setText(btn, theTitle);
913 btn.onClick = theFunc;
914 btn.onClickEntity = me;
917 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
919 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
921 me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
922 me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
923 me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
925 me.columnIconsOrigin = 0;
926 me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
927 me.columnPingSize = me.realFontSize.x * 3;
928 me.columnMapSize = me.realFontSize.x * 10;
929 me.columnTypeSize = me.realFontSize.x * 4;
930 me.columnPlayersSize = me.realFontSize.x * 5;
931 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
932 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
933 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
934 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
935 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
936 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
938 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
939 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
940 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
941 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
942 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
944 int f = me.currentSortField;
947 me.currentSortField = -1;
948 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
951 void ServerList_Connect_Click(entity btn, entity me)
953 localcmd(sprintf("connect %s\n",
954 ((me.ipAddressBox.text != "") ?
955 me.ipAddressBox.text : me.selectedServer
959 void ServerList_Favorite_Click(entity btn, entity me)
962 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
965 m_play_click_sound(MENU_SOUND_SELECT);
966 me.toggleFavorite(me, me.ipAddressBox.text);
967 me.ipAddressBoxFocused = -1;
970 void ServerList_Info_Click(entity btn, entity me)
973 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
975 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
976 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
977 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
979 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
981 ServerList_Connect_Click(NULL, me);
983 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
985 // layout: Ping, Server name, Map name, NP, TP, MP
992 int freeslots = -1, sflags = -1, j, m;
993 string s, typestr, versionstr, k, v, modname;
995 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
997 vector oldscale = draw_scale;
998 vector oldshift = draw_shift;
999 #define SET_YRANGE(start,end) \
1000 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
1001 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
1003 for (j = 0; j < category_draw_count; ++j) {
1004 // Matches exactly the headings with increased height.
1005 if (i == category_item[j])
1009 if (j < category_draw_count)
1011 entity catent = RetrieveCategoryEnt(category_name[j]);
1015 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1016 me.categoriesHeight / (me.categoriesHeight + 1)
1019 eY * me.realUpperMargin
1022 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1025 eX * (me.columnNameOrigin),
1026 strcat(catent.cat_string, ":"),
1029 SKINCOLOR_SERVERLIST_CATEGORY,
1030 SKINALPHA_SERVERLIST_CATEGORY,
1033 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1038 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1041 me.focusedItemAlpha = getFadedAlpha(me.focusedItemAlpha, SKINALPHA_LISTBOX_FOCUSED, SKINFADEALPHA_LISTBOX_FOCUSED);
1042 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_FOCUSED, me.focusedItemAlpha);
1045 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1046 m = tokenizebyseparator(s, ":");
1051 versionstr = argv(1);
1055 for(j = 2; j < m; ++j)
1059 k = substring(argv(j), 0, 1);
1060 v = substring(argv(j), 1, -1);
1064 freeslots = stof(v);
1071 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1073 modname = "Xonotic";
1077 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1078 s = gethostcachestring(SLIST_FIELD_MOD, i);
1080 if(modname == "Xonotic")
1084 // list the mods here on which the pure server check actually works
1085 if(modname != "Xonotic")
1086 if(modname != "InstaGib" || modname != "MinstaGib")
1087 if(modname != "CTS")
1088 if(modname != "NIX")
1089 if(modname != "NewToys")
1092 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1093 theAlpha = SKINALPHA_SERVERLIST_FULL;
1094 else if(freeslots == 0)
1095 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1096 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1097 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1101 p = gethostcachenumber(SLIST_FIELD_PING, i);
1102 const int PING_LOW = 75;
1103 const int PING_MED = 200;
1104 const int PING_HIGH = 500;
1106 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1107 else if(p < PING_MED)
1108 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1109 else if(p < PING_HIGH)
1111 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1112 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1117 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1120 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1122 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1123 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1126 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1128 isv4 = isv6 = false;
1129 if(substring(s, 0, 1) == "[")
1134 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1140 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1141 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1143 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1144 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1149 if(cvar("crypto_aeslevel") >= 2)
1154 if(cvar("crypto_aeslevel") >= 1)
1164 // 2: AES recommended but not available
1165 // 3: AES possible and will be used
1166 // 4: AES recommended and will be used
1172 vector iconSize = '0 0 0';
1173 iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1174 iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1176 vector iconPos = '0 0 0';
1177 iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1178 iconPos_y = (1 - iconSize.y) * 0.5;
1181 if(me.seenIPv4 && me.seenIPv6)
1184 draw_Picture(iconPos, "icon_ipv6", iconSize, '1 1 1', 1);
1186 draw_Picture(iconPos, "icon_ipv4", iconSize, '1 1 1', 1);
1189 iconPos.x += iconSize.x;
1193 draw_Picture(iconPos, strcat("icon_aeslevel", ftos(q)), iconSize, '1 1 1', 1);
1195 iconPos.x += iconSize.x;
1198 if(modname == "Xonotic")
1201 draw_Picture(iconPos, "icon_pure1", iconSize, '1 1 1', 1);
1205 string icon = strcat("icon_mod_", modname);
1206 if(draw_PictureSize(icon) == '0 0 0')
1210 draw_Picture(iconPos, icon, iconSize, '1 1 1', 1);
1212 draw_Picture(iconPos, icon, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1215 iconPos.x += iconSize.x;
1218 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1219 draw_Picture(iconPos, "icon_stats1", iconSize, '1 1 1', 1);
1227 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1230 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1231 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1234 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1235 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1238 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1239 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1241 // server playercount
1242 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1243 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1246 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1250 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1251 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1253 if(scan == K_ENTER || scan == K_KP_ENTER)
1255 ServerList_Connect_Click(NULL, me);
1258 else if(scan == K_MOUSE2 || scan == K_SPACE)
1262 m_play_click_sound(MENU_SOUND_OPEN);
1263 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1264 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1269 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1273 me.toggleFavorite(me, me.selectedServer);
1274 me.ipAddressBoxFocused = -1;
1279 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1281 else if(!me.controlledTextbox)
1284 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1287 float XonoticServerList_getTotalHeight(entity me)
1289 float num_normal_rows = me.nItems;
1290 int num_headers = category_draw_count;
1291 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1293 int XonoticServerList_getItemAtPos(entity me, float pos)
1295 pos = pos / me.itemHeight;
1297 for (i = category_draw_count - 1; i >= 0; --i) {
1298 int itemidx = category_item[i];
1299 float itempos = i * me.categoriesHeight + category_item[i];
1300 if (pos >= itempos + me.categoriesHeight + 1)
1301 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1305 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1308 float XonoticServerList_getItemStart(entity me, int item)
1311 for (i = category_draw_count - 1; i >= 0; --i) {
1312 int itemidx = category_item[i];
1313 float itempos = i * me.categoriesHeight + category_item[i];
1314 if (item >= itemidx + 1)
1315 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1316 if (item >= itemidx)
1317 return itempos * me.itemHeight;
1319 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1320 return item * me.itemHeight;
1322 float XonoticServerList_getItemHeight(entity me, int item)
1325 for (i = 0; i < category_draw_count; ++i) {
1326 // Matches exactly the headings with increased height.
1327 if (item == category_item[i])
1328 return me.itemHeight * (me.categoriesHeight + 1);
1330 return me.itemHeight;