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, int, vector, bool, bool))
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 int MAX_CATEGORIES = 9;
130 const int CATEGORY_FIRST = 1;
131 entity categories[MAX_CATEGORIES];
132 int 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 int category_name[MAX_CATEGORIES];
142 int category_item[MAX_CATEGORIES];
143 int 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(int 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 bool 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 int CheckCategoryOverride(int cat)
265 entity catent = RetrieveCategoryEnt(cat);
268 int 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 int CheckCategoryForEntry(int entry)
281 string s, k, v, modtype = "";
282 int 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;
386 srv_resolved = netaddress_resolve(srv, 26000);
387 p = crypto_getidfp(srv_resolved);
388 s = cvar_string("net_slist_favorites");
389 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, int i)
461 //int save = me.selectedItem;
462 SUPER(XonoticServerList).setSelected(me, i);
464 if(me.selectedItem == save)
469 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
470 return; // sorry, it would be wrong
472 if(me.selectedServer)
473 strunzone(me.selectedServer);
474 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
476 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
477 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
478 me.ipAddressBoxFocused = -1;
480 void XonoticServerList_refreshServerList(entity me, int mode)
482 //print("refresh of type ", ftos(mode), "\n");
484 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 SUPER(XonoticServerList).focusEnter(me);
565 if(time < me.nextRefreshTime)
567 //print("sorry, no refresh yet\n");
570 me.nextRefreshTime = time + 10;
571 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
574 void XonoticServerList_draw(entity me)
577 bool found = false, owned;
579 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
583 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
586 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
590 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
593 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
597 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
600 if(me.currentSortField == -1)
602 me.setSortOrder(me, SLIST_FIELD_PING, +1);
603 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
605 else if(me.needsRefresh == 1)
607 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
609 else if(me.needsRefresh == 2)
612 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
614 else if(me.needsRefresh == 3)
617 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
620 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
622 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
623 category_draw_count = 0;
625 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
627 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
628 me.nItems = itemcount;
630 //float visible = floor(me.scrollPos / me.itemHeight);
631 // ^ unfortunately no such optimization can be made-- we must process through the
632 // entire list, otherwise there is no way to know which item is first in its category.
634 // binary search method suggested by div
637 for(x = 1; x <= category_ent_count; ++x) {
639 float last = (itemcount - 1);
644 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
645 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
647 // The first one is already > x.
648 // Therefore, category x does not exist.
649 // Higher numbered categories do exist though.
650 } else if (catl < x) {
651 // The last one is < x.
652 // Thus this category - and any following -
655 } else if (catf == x) {
656 // Starts at first. This breaks the loop
657 // invariant in the binary search and thus has
658 // to be handled separately.
659 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
660 error("Category mismatch I");
662 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
663 error("Category mismatch II");
664 category_name[category_draw_count] = x;
665 category_item[category_draw_count] = first;
666 ++category_draw_count;
669 // At this point, catf <= x < catl, thus
670 // catf < catl, thus first < last.
673 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
674 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
677 while (last - first > 1) {
678 float middle = floor((first + last) / 2);
679 // By loop condition, middle != first && middle != last.
680 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
690 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
691 error("Category mismatch III");
693 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
694 error("Category mismatch IV");
695 category_name[category_draw_count] = x;
696 category_item[category_draw_count] = last;
697 ++category_draw_count;
698 begin = last + 1; // already scanned through these, skip 'em
701 begin = last; // already scanned through these, skip 'em
704 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
706 category_name[0] = -1;
707 category_item[0] = -1;
708 category_draw_count = 0;
709 me.nItems = itemcount;
712 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
714 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
715 me.infoButton.disabled = ((me.nItems == 0) || !owned);
716 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
718 if(me.selectedServer)
720 for(i = 0; i < me.nItems; ++i)
722 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
734 if(me.selectedItem >= me.nItems)
735 me.selectedItem = me.nItems - 1;
736 if(me.selectedServer)
737 strunzone(me.selectedServer);
738 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
744 if(me.selectedServer != me.ipAddressBox.text)
746 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
747 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
748 me.ipAddressBoxFocused = -1;
752 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
754 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
755 ServerList_Update_favoriteButton(NULL, me);
756 me.ipAddressBoxFocused = me.ipAddressBox.focused;
759 SUPER(XonoticServerList).draw(me);
761 void ServerList_PingSort_Click(entity btn, entity me)
763 me.setSortOrder(me, SLIST_FIELD_PING, +1);
765 void ServerList_NameSort_Click(entity btn, entity me)
767 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
769 void ServerList_MapSort_Click(entity btn, entity me)
771 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
773 void ServerList_PlayerSort_Click(entity btn, entity me)
775 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
777 void ServerList_TypeSort_Click(entity btn, entity me)
782 m = strstrofs(s, ":", 0);
785 s = substring(s, 0, m);
786 while(substring(s, m+1, 1) == " ") // skip spaces
792 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
794 t = MapInfo_Type_ToString(i);
796 if(t == "") // it repeats (default case)
799 // choose the first one
800 s = MapInfo_Type_ToString(1);
805 // the type was found
806 // choose the next one
807 s = MapInfo_Type_ToString(i * 2);
809 s = MapInfo_Type_ToString(1);
816 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
818 me.controlledTextbox.setText(me.controlledTextbox, s);
819 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
820 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
821 //ServerList_Filter_Change(me.controlledTextbox, me);
823 void ServerList_Filter_Change(entity box, entity me)
826 strunzone(me.filterString);
828 me.filterString = strzone(box.text);
830 me.filterString = string_null;
831 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
833 me.ipAddressBox.setText(me.ipAddressBox, "");
834 me.ipAddressBox.cursorPos = 0;
835 me.ipAddressBoxFocused = -1;
837 void ServerList_Categories_Click(entity box, entity me)
839 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
840 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
842 me.ipAddressBox.setText(me.ipAddressBox, "");
843 me.ipAddressBox.cursorPos = 0;
844 me.ipAddressBoxFocused = -1;
846 void ServerList_ShowEmpty_Click(entity box, entity me)
848 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
849 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
851 me.ipAddressBox.setText(me.ipAddressBox, "");
852 me.ipAddressBox.cursorPos = 0;
853 me.ipAddressBoxFocused = -1;
855 void ServerList_ShowFull_Click(entity box, entity me)
857 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
858 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
860 me.ipAddressBox.setText(me.ipAddressBox, "");
861 me.ipAddressBox.cursorPos = 0;
862 me.ipAddressBoxFocused = -1;
864 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
866 if(me.currentSortField == fld)
867 direction = -me.currentSortOrder;
868 me.currentSortOrder = direction;
869 me.currentSortField = fld;
870 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
871 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
872 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
873 me.sortButton4.forcePressed = 0;
874 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
876 if(me.selectedServer)
877 strunzone(me.selectedServer);
878 me.selectedServer = string_null;
879 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
881 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
883 vector originInLBSpace, sizeInLBSpace;
884 originInLBSpace = eY * (-me.itemHeight);
885 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
887 vector originInDialogSpace, sizeInDialogSpace;
888 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
889 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
891 btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
892 btn.Container_size_x = sizeInDialogSpace.x * theSize;
893 btn.setText(btn, theTitle);
894 btn.onClick = theFunc;
895 btn.onClickEntity = me;
898 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
900 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
902 me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
903 me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
904 me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
906 me.columnIconsOrigin = 0;
907 me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
908 me.columnPingSize = me.realFontSize.x * 3;
909 me.columnMapSize = me.realFontSize.x * 10;
910 me.columnTypeSize = me.realFontSize.x * 4;
911 me.columnPlayersSize = me.realFontSize.x * 5;
912 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
913 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
914 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
915 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
916 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
917 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
919 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
920 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
921 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
922 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
923 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
925 int 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 m_play_click_sound(MENU_SOUND_SELECT);
947 me.toggleFavorite(me, me.ipAddressBox.text);
948 me.ipAddressBoxFocused = -1;
951 void ServerList_Info_Click(entity btn, entity me)
954 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
956 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
957 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
958 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
960 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
962 ServerList_Connect_Click(NULL, me);
964 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
966 // layout: Ping, Server name, Map name, NP, TP, MP
973 int freeslots = -1, sflags = -1, j, m;
974 string s, typestr, versionstr, k, v, modname;
976 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
978 vector oldscale = draw_scale;
979 vector oldshift = draw_shift;
980 #define SET_YRANGE(start,end) \
981 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
982 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
984 for (j = 0; j < category_draw_count; ++j) {
985 // Matches exactly the headings with increased height.
986 if (i == category_item[j])
990 if (j < category_draw_count)
992 entity catent = RetrieveCategoryEnt(category_name[j]);
996 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
997 me.categoriesHeight / (me.categoriesHeight + 1)
1000 eY * me.realUpperMargin
1003 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1006 eX * (me.columnNameOrigin),
1007 strcat(catent.cat_string, ":"),
1010 SKINCOLOR_SERVERLIST_CATEGORY,
1011 SKINALPHA_SERVERLIST_CATEGORY,
1014 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1019 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1022 me.focusedItemAlpha = getFadedAlpha(me.focusedItemAlpha, SKINALPHA_LISTBOX_FOCUSED, SKINFADEALPHA_LISTBOX_FOCUSED);
1023 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_FOCUSED, me.focusedItemAlpha);
1026 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1027 m = tokenizebyseparator(s, ":");
1032 versionstr = argv(1);
1036 for(j = 2; j < m; ++j)
1040 k = substring(argv(j), 0, 1);
1041 v = substring(argv(j), 1, -1);
1045 freeslots = stof(v);
1052 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1054 modname = "Xonotic";
1058 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1059 s = gethostcachestring(SLIST_FIELD_MOD, i);
1061 if(modname == "Xonotic")
1065 // list the mods here on which the pure server check actually works
1066 if(modname != "Xonotic")
1067 if(modname != "InstaGib" || modname != "MinstaGib")
1068 if(modname != "CTS")
1069 if(modname != "NIX")
1070 if(modname != "NewToys")
1073 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1074 theAlpha = SKINALPHA_SERVERLIST_FULL;
1075 else if(freeslots == 0)
1076 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1077 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1078 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1082 p = gethostcachenumber(SLIST_FIELD_PING, i);
1083 const int PING_LOW = 75;
1084 const int PING_MED = 200;
1085 const int PING_HIGH = 500;
1087 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1088 else if(p < PING_MED)
1089 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1090 else if(p < PING_HIGH)
1092 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1093 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1098 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1101 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1103 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1104 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1107 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1109 isv4 = isv6 = false;
1110 if(substring(s, 0, 1) == "[")
1115 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1121 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1122 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1124 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1125 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1130 if(cvar("crypto_aeslevel") >= 2)
1135 if(cvar("crypto_aeslevel") >= 1)
1145 // 2: AES recommended but not available
1146 // 3: AES possible and will be used
1147 // 4: AES recommended and will be used
1153 vector iconSize = '0 0 0';
1154 iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1155 iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1157 vector iconPos = '0 0 0';
1158 iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1159 iconPos_y = (1 - iconSize.y) * 0.5;
1163 if (!(me.seenIPv4 && me.seenIPv6))
1165 iconPos.x += iconSize.x * 0.5;
1167 else if(me.seenIPv4 && me.seenIPv6)
1171 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1173 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1175 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1176 iconPos.x += iconSize.x;
1181 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1182 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1184 iconPos.x += iconSize.x;
1186 if(modname == "Xonotic")
1190 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1191 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1196 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1197 if(draw_PictureSize(n) == '0 0 0')
1198 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1200 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1202 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1204 iconPos.x += iconSize.x;
1206 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1208 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1209 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1211 iconPos.x += iconSize.x;
1219 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1222 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1223 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1226 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1227 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1230 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1231 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1233 // server playercount
1234 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1235 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1238 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1242 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1243 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1245 if(scan == K_ENTER || scan == K_KP_ENTER)
1247 ServerList_Connect_Click(NULL, me);
1250 else if(scan == K_MOUSE2 || scan == K_SPACE)
1254 m_play_click_sound(MENU_SOUND_OPEN);
1255 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1256 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1261 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1265 me.toggleFavorite(me, me.selectedServer);
1266 me.ipAddressBoxFocused = -1;
1271 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1273 else if(!me.controlledTextbox)
1276 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1279 float XonoticServerList_getTotalHeight(entity me)
1281 float num_normal_rows = me.nItems;
1282 int num_headers = category_draw_count;
1283 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1285 int XonoticServerList_getItemAtPos(entity me, float pos)
1287 pos = pos / me.itemHeight;
1289 for (i = category_draw_count - 1; i >= 0; --i) {
1290 int itemidx = category_item[i];
1291 float itempos = i * me.categoriesHeight + category_item[i];
1292 if (pos >= itempos + me.categoriesHeight + 1)
1293 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1297 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1300 float XonoticServerList_getItemStart(entity me, int item)
1303 for (i = category_draw_count - 1; i >= 0; --i) {
1304 int itemidx = category_item[i];
1305 float itempos = i * me.categoriesHeight + category_item[i];
1306 if (item >= itemidx + 1)
1307 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1308 if (item >= itemidx)
1309 return itempos * me.itemHeight;
1311 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1312 return item * me.itemHeight;
1314 float XonoticServerList_getItemHeight(entity me, int item)
1317 for (i = 0; i < category_draw_count; ++i) {
1318 // Matches exactly the headings with increased height.
1319 if (item == category_item[i])
1320 return me.itemHeight * (me.categoriesHeight + 1);
1322 return me.itemHeight;