2 CLASS(XonoticServerList) EXTENDS(XonoticListBox)
3 METHOD(XonoticServerList, configureXonoticServerList, void(entity))
4 ATTRIB(XonoticServerList, rowsPerItem, float, 1)
5 METHOD(XonoticServerList, draw, void(entity))
6 METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
7 METHOD(XonoticServerList, clickListBoxItem, void(entity, float, vector))
8 METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
9 METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
10 METHOD(XonoticServerList, toggleFavorite, void(entity, string))
12 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
14 ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
15 ATTRIB(XonoticServerList, realUpperMargin, float, 0)
16 ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
17 ATTRIB(XonoticServerList, columnIconsSize, float, 0)
18 ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
19 ATTRIB(XonoticServerList, columnPingSize, float, 0)
20 ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
21 ATTRIB(XonoticServerList, columnNameSize, float, 0)
22 ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
23 ATTRIB(XonoticServerList, columnMapSize, float, 0)
24 ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
25 ATTRIB(XonoticServerList, columnTypeSize, float, 0)
26 ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
27 ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
29 ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
30 METHOD(XonoticServerList, setSelected, void(entity, float))
31 METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
32 ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
33 ATTRIB(XonoticServerList, filterShowFull, float, 1)
34 ATTRIB(XonoticServerList, filterString, string, string_null)
35 ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
36 ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
37 ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
38 ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
39 METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
40 ATTRIB(XonoticServerList, needsRefresh, float, 1)
41 METHOD(XonoticServerList, focusEnter, void(entity))
42 METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
43 ATTRIB(XonoticServerList, sortButton1, entity, NULL)
44 ATTRIB(XonoticServerList, sortButton2, entity, NULL)
45 ATTRIB(XonoticServerList, sortButton3, entity, NULL)
46 ATTRIB(XonoticServerList, sortButton4, entity, NULL)
47 ATTRIB(XonoticServerList, sortButton5, entity, NULL)
48 ATTRIB(XonoticServerList, connectButton, entity, NULL)
49 ATTRIB(XonoticServerList, infoButton, entity, NULL)
50 ATTRIB(XonoticServerList, currentSortOrder, float, 0)
51 ATTRIB(XonoticServerList, currentSortField, float, -1)
52 ATTRIB(XonoticServerList, lastClickedServer, float, -1)
53 ATTRIB(XonoticServerList, lastClickedTime, float, 0)
55 ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
57 ATTRIB(XonoticServerList, seenIPv4, float, 0)
58 ATTRIB(XonoticServerList, seenIPv6, float, 0)
59 ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
61 METHOD(XonoticServerList, getTotalHeight, float(entity))
62 METHOD(XonoticServerList, getItemAtPos, float(entity, float))
63 METHOD(XonoticServerList, getItemStart, float(entity, float))
64 METHOD(XonoticServerList, getItemHeight, float(entity, float))
65 ENDCLASS(XonoticServerList)
66 entity makeXonoticServerList();
68 #ifndef IMPLEMENTATION
69 float autocvar_menu_slist_categories;
70 float autocvar_menu_slist_categories_onlyifmultiple;
71 float autocvar_menu_slist_purethreshold;
72 float autocvar_menu_slist_modimpurity;
73 float autocvar_menu_slist_recommendations;
74 float autocvar_menu_slist_recommendations_maxping;
75 float autocvar_menu_slist_recommendations_minfreeslots;
76 float autocvar_menu_slist_recommendations_minhumans;
77 float autocvar_menu_slist_recommendations_purethreshold;
79 // server cache fields
80 #define SLIST_FIELDS \
81 SLIST_FIELD(CNAME, "cname") \
82 SLIST_FIELD(PING, "ping") \
83 SLIST_FIELD(GAME, "game") \
84 SLIST_FIELD(MOD, "mod") \
85 SLIST_FIELD(MAP, "map") \
86 SLIST_FIELD(NAME, "name") \
87 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
88 SLIST_FIELD(NUMPLAYERS, "numplayers") \
89 SLIST_FIELD(NUMHUMANS, "numhumans") \
90 SLIST_FIELD(NUMBOTS, "numbots") \
91 SLIST_FIELD(PROTOCOL, "protocol") \
92 SLIST_FIELD(FREESLOTS, "freeslots") \
93 SLIST_FIELD(PLAYERS, "players") \
94 SLIST_FIELD(QCSTATUS, "qcstatus") \
95 SLIST_FIELD(CATEGORY, "category") \
96 SLIST_FIELD(ISFAVORITE, "isfavorite")
98 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
102 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
103 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
104 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
105 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
107 // function declarations
108 float IsServerInList(string list, string srv);
109 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
110 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
111 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
113 entity RetrieveCategoryEnt(float catnum);
115 float CheckCategoryOverride(float cat);
116 float CheckCategoryForEntry(float entry);
117 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
119 void RegisterSLCategories();
121 void ServerList_Connect_Click(entity btn, entity me);
122 void ServerList_Categories_Click(entity box, entity me);
123 void ServerList_ShowEmpty_Click(entity box, entity me);
124 void ServerList_ShowFull_Click(entity box, entity me);
125 void ServerList_Filter_Change(entity box, entity me);
126 void ServerList_Favorite_Click(entity btn, entity me);
127 void ServerList_Info_Click(entity btn, entity me);
128 void ServerList_Update_favoriteButton(entity btn, entity me);
130 // fields for category entities
131 #define MAX_CATEGORIES 9
132 #define CATEGORY_FIRST 1
133 entity categories[MAX_CATEGORIES];
134 float category_ent_count;
137 .string cat_enoverride_string;
138 .string cat_dioverride_string;
139 .float cat_enoverride;
140 .float cat_dioverride;
142 // fields for drawing categories
143 float category_name[MAX_CATEGORIES];
144 float category_item[MAX_CATEGORIES];
145 float category_draw_count;
147 #define SLIST_CATEGORIES \
148 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
149 SLIST_CATEGORY(CAT_RECOMMENDED, "", "", ZCTX(_("SLCAT^Recommended"))) \
150 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
151 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
152 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
153 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
154 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
155 SLIST_CATEGORY(CAT_INSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^InstaGib Mode"))) \
156 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
158 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
159 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
161 var string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
163 #undef SLIST_CATEGORY
167 #ifdef IMPLEMENTATION
169 void RegisterSLCategories()
172 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
173 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
174 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
176 categories[name - 1] = cat; \
177 cat.classname = "slist_category"; \
178 cat.cat_name = strzone(#name); \
179 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
180 cat.cat_dioverride_string = strzone(dioverride); \
181 cat.cat_string = strzone(str);
183 #undef SLIST_CATEGORY
188 #define PROCESS_OVERRIDE(override_string,override_field) \
189 for(i = 0; i < category_ent_count; ++i) \
191 s = categories[i].override_string; \
192 if((s != "") && (s != categories[i].cat_name)) \
195 for(x = 0; x < category_ent_count; ++x) \
196 { if(categories[x].cat_name == s) { \
202 strunzone(categories[i].override_string); \
203 categories[i].override_field = catnum; \
209 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
211 categories[i].cat_name \
215 strunzone(categories[i].override_string); \
216 categories[i].override_field = 0; \
218 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
219 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
220 #undef PROCESS_OVERRIDE
223 // Supporting Functions
224 entity RetrieveCategoryEnt(float catnum)
226 if((catnum > 0) && (catnum <= category_ent_count))
228 return categories[catnum - 1];
232 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
237 float IsServerInList(string list, string srv)
243 srv = netaddress_resolve(srv, 26000);
246 p = crypto_getidfp(srv);
247 n = tokenize_console(list);
248 for(i = 0; i < n; ++i)
250 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
258 if(srv == netaddress_resolve(argv(i), 26000))
265 float CheckCategoryOverride(float cat)
267 entity catent = RetrieveCategoryEnt(cat);
270 float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
271 if(override) { return override; }
276 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
281 float CheckCategoryForEntry(float entry)
283 string s, k, v, modtype = "";
284 float j, m, impure = 0, freeslots = 0, sflags = 0;
285 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
286 m = tokenizebyseparator(s, ":");
288 for(j = 2; j < m; ++j)
290 if(argv(j) == "") { break; }
291 k = substring(argv(j), 0, 1);
292 v = substring(argv(j), 1, -1);
295 case "P": { impure = stof(v); break; }
296 case "S": { freeslots = stof(v); break; }
297 case "F": { sflags = stof(v); break; }
298 case "M": { modtype = strtolower(v); break; }
302 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
304 // check if this server is favorited
305 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
307 // now check if it's recommended
308 if(autocvar_menu_slist_recommendations)
310 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
312 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
315 float recommended = 0;
316 if(autocvar_menu_slist_recommendations & 1)
318 if(IsRecommended(cname)) { ++recommended; }
319 else { --recommended; }
321 if(autocvar_menu_slist_recommendations & 2)
324 ///// check for minimum free slots
325 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
327 && // check for purity requirement
329 (autocvar_menu_slist_recommendations_purethreshold < 0)
331 (impure <= autocvar_menu_slist_recommendations_purethreshold)
334 && // check for minimum amount of humans
336 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
338 autocvar_menu_slist_recommendations_minhumans
341 && // check for maximum latency
343 gethostcachenumber(SLIST_FIELD_PING, entry)
345 autocvar_menu_slist_recommendations_maxping
352 if(recommended > 0) { return CAT_RECOMMENDED; }
356 // if not favorited or recommended, check modname
357 if(modtype != "xonotic")
361 // old servers which don't report their mod name are considered modified now
362 case "": { return CAT_MODIFIED; }
364 case "xpm": { return CAT_XPM; }
366 case "instagib": { return CAT_INSTAGIB; }
367 case "overkill": { return CAT_OVERKILL; }
368 //case "nix": { return CAT_NIX; }
369 //case "newtoys": { return CAT_NEWTOYS; }
371 // "cts" is allowed as compat, xdf is replacement
373 case "xdf": { return CAT_DEFRAG; }
375 default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
379 // must be normal or impure server
380 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
383 void XonoticServerList_toggleFavorite(entity me, string srv)
385 string s, s0, s1, s2, srv_resolved, p;
387 srv_resolved = netaddress_resolve(srv, 26000);
388 p = crypto_getidfp(srv_resolved);
389 s = cvar_string("net_slist_favorites");
390 n = tokenize_console(s);
392 for(i = 0; i < n; ++i)
394 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
402 if(srv_resolved != netaddress_resolve(argv(i), 26000))
407 s0 = substring(s, 0, argv_end_index(i - 1));
409 s2 = substring(s, argv_start_index(i + 1), -1);
410 if(s0 != "" && s2 != "")
412 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
413 s = cvar_string("net_slist_favorites");
414 n = tokenize_console(s);
425 cvar_set("net_slist_favorites", strcat(s, s1, p));
427 cvar_set("net_slist_favorites", strcat(s, s1, srv));
430 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
433 void ServerList_Update_favoriteButton(entity btn, entity me)
435 me.favoriteButton.setText(me.favoriteButton,
436 (IsFavorite(me.ipAddressBox.text) ?
437 _("Remove") : _("Favorite")
442 entity makeXonoticServerList()
445 me = spawnXonoticServerList();
446 me.configureXonoticServerList(me);
449 void XonoticServerList_configureXonoticServerList(entity me)
451 me.configureXonoticListBox(me);
454 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
461 void XonoticServerList_setSelected(entity me, float i)
464 save = me.selectedItem;
465 SUPER(XonoticServerList).setSelected(me, i);
467 if(me.selectedItem == save)
472 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
473 return; // sorry, it would be wrong
475 if(me.selectedServer)
476 strunzone(me.selectedServer);
477 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
479 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
480 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
481 me.ipAddressBoxFocused = -1;
483 void XonoticServerList_refreshServerList(entity me, float mode)
485 //print("refresh of type ", ftos(mode), "\n");
487 if(mode >= REFRESHSERVERLIST_REFILTER)
491 string s, typestr, modstr;
495 m = strstrofs(s, ":", 0);
498 typestr = substring(s, 0, m);
499 s = substring(s, m + 1, strlen(s) - m - 1);
500 while(substring(s, 0, 1) == " ")
501 s = substring(s, 1, strlen(s) - 1);
506 modstr = cvar_string("menu_slist_modfilter");
508 m = SLIST_MASK_AND - 1;
509 resethostcachemasks();
511 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
512 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
515 if(!me.filterShowFull)
517 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
518 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
522 if(!me.filterShowEmpty)
523 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
525 // gametype filtering
527 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
532 if(substring(modstr, 0, 1) == "!")
533 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
535 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
539 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
540 for(i = 0; i < n; ++i)
542 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
544 m = SLIST_MASK_OR - 1;
547 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
548 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
549 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
550 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
554 //listflags |= SLSF_FAVORITES;
555 listflags |= SLSF_CATEGORIES;
556 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
557 sethostcachesort(me.currentSortField, listflags);
561 if(mode >= REFRESHSERVERLIST_ASK)
562 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
564 void XonoticServerList_focusEnter(entity me)
566 if(time < me.nextRefreshTime)
568 //print("sorry, no refresh yet\n");
571 me.nextRefreshTime = time + 10;
572 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
575 void XonoticServerList_draw(entity me)
577 float i, found, 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 == ""));
719 if(me.selectedServer)
721 for(i = 0; i < me.nItems; ++i)
723 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
725 if(i != me.selectedItem)
727 me.lastClickedServer = -1;
739 if(me.selectedItem >= me.nItems)
740 me.selectedItem = me.nItems - 1;
741 if(me.selectedServer)
742 strunzone(me.selectedServer);
743 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
749 if(me.selectedServer != me.ipAddressBox.text)
751 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
752 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
753 me.ipAddressBoxFocused = -1;
757 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
759 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
760 ServerList_Update_favoriteButton(NULL, me);
761 me.ipAddressBoxFocused = me.ipAddressBox.focused;
764 SUPER(XonoticServerList).draw(me);
766 void ServerList_PingSort_Click(entity btn, entity me)
768 me.setSortOrder(me, SLIST_FIELD_PING, +1);
770 void ServerList_NameSort_Click(entity btn, entity me)
772 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
774 void ServerList_MapSort_Click(entity btn, entity me)
776 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
778 void ServerList_PlayerSort_Click(entity btn, entity me)
780 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
782 void ServerList_TypeSort_Click(entity btn, entity me)
787 m = strstrofs(s, ":", 0);
790 s = substring(s, 0, m);
791 while(substring(s, m+1, 1) == " ") // skip spaces
797 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
799 t = MapInfo_Type_ToString(i);
801 if(t == "") // it repeats (default case)
804 // choose the first one
805 s = MapInfo_Type_ToString(1);
810 // the type was found
811 // choose the next one
812 s = MapInfo_Type_ToString(i * 2);
814 s = MapInfo_Type_ToString(1);
821 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
823 me.controlledTextbox.setText(me.controlledTextbox, s);
824 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
825 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
826 //ServerList_Filter_Change(me.controlledTextbox, me);
828 void ServerList_Filter_Change(entity box, entity me)
831 strunzone(me.filterString);
833 me.filterString = strzone(box.text);
835 me.filterString = string_null;
836 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
838 me.ipAddressBox.setText(me.ipAddressBox, "");
839 me.ipAddressBox.cursorPos = 0;
840 me.ipAddressBoxFocused = -1;
842 void ServerList_Categories_Click(entity box, entity me)
844 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
845 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
847 me.ipAddressBox.setText(me.ipAddressBox, "");
848 me.ipAddressBox.cursorPos = 0;
849 me.ipAddressBoxFocused = -1;
851 void ServerList_ShowEmpty_Click(entity box, entity me)
853 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
854 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
856 me.ipAddressBox.setText(me.ipAddressBox, "");
857 me.ipAddressBox.cursorPos = 0;
858 me.ipAddressBoxFocused = -1;
860 void ServerList_ShowFull_Click(entity box, entity me)
862 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
863 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
865 me.ipAddressBox.setText(me.ipAddressBox, "");
866 me.ipAddressBox.cursorPos = 0;
867 me.ipAddressBoxFocused = -1;
869 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
871 if(me.currentSortField == fld)
872 direction = -me.currentSortOrder;
873 me.currentSortOrder = direction;
874 me.currentSortField = fld;
875 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
876 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
877 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
878 me.sortButton4.forcePressed = 0;
879 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
881 if(me.selectedServer)
882 strunzone(me.selectedServer);
883 me.selectedServer = string_null;
884 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
886 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
888 vector originInLBSpace, sizeInLBSpace;
889 originInLBSpace = eY * (-me.itemHeight);
890 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
892 vector originInDialogSpace, sizeInDialogSpace;
893 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
894 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
896 btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
897 btn.Container_size_x = sizeInDialogSpace_x * theSize;
898 btn.setText(btn, theTitle);
899 btn.onClick = theFunc;
900 btn.onClickEntity = me;
903 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
905 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
907 me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
908 me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
909 me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
911 me.columnIconsOrigin = 0;
912 me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
913 me.columnPingSize = me.realFontSize_x * 3;
914 me.columnMapSize = me.realFontSize_x * 10;
915 me.columnTypeSize = me.realFontSize_x * 4;
916 me.columnPlayersSize = me.realFontSize_x * 5;
917 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
918 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
919 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
920 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
921 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
922 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
924 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
925 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
926 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
927 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
928 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
931 f = me.currentSortField;
934 me.currentSortField = -1;
935 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
938 void ServerList_Connect_Click(entity btn, entity me)
940 localcmd(sprintf("connect %s\n",
941 ((me.ipAddressBox.text != "") ?
942 me.ipAddressBox.text : me.selectedServer
946 void ServerList_Favorite_Click(entity btn, entity me)
949 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
952 me.toggleFavorite(me, me.ipAddressBox.text);
953 me.ipAddressBoxFocused = -1;
956 void ServerList_Info_Click(entity btn, entity me)
959 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
961 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
962 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
963 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
965 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
967 if(i == me.lastClickedServer)
968 if(time < me.lastClickedTime + 0.3)
971 ServerList_Connect_Click(NULL, me);
973 me.lastClickedServer = i;
974 me.lastClickedTime = time;
976 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
978 // layout: Ping, Server name, Map name, NP, TP, MP
983 float m, pure, freeslots, j, sflags;
984 string s, typestr, versionstr, k, v, modname;
986 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
988 vector oldscale = draw_scale;
989 vector oldshift = draw_shift;
990 #define SET_YRANGE(start,end) \
991 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
992 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
994 for (j = 0; j < category_draw_count; ++j) {
995 // Matches exactly the headings with increased height.
996 if (i == category_item[j])
1000 if (j < category_draw_count)
1002 entity catent = RetrieveCategoryEnt(category_name[j]);
1006 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1007 me.categoriesHeight / (me.categoriesHeight + 1)
1010 eY * me.realUpperMargin
1013 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1016 eX * (me.columnNameOrigin),
1017 strcat(catent.cat_string, ":"),
1020 SKINCOLOR_SERVERLIST_CATEGORY,
1021 SKINALPHA_SERVERLIST_CATEGORY,
1024 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1029 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1031 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1032 m = tokenizebyseparator(s, ":");
1037 versionstr = argv(1);
1043 for(j = 2; j < m; ++j)
1047 k = substring(argv(j), 0, 1);
1048 v = substring(argv(j), 1, -1);
1052 freeslots = stof(v);
1059 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1061 modname = "Xonotic";
1065 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1066 s = gethostcachestring(SLIST_FIELD_MOD, i);
1068 if(modname == "Xonotic")
1072 // list the mods here on which the pure server check actually works
1073 if(modname != "Xonotic")
1074 if(modname != "InstaGib" || modname != "MinstaGib")
1075 if(modname != "CTS")
1076 if(modname != "NIX")
1077 if(modname != "NewToys")
1080 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1081 theAlpha = SKINALPHA_SERVERLIST_FULL;
1082 else if(freeslots == 0)
1083 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1084 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1085 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1089 p = gethostcachenumber(SLIST_FIELD_PING, i);
1091 #define PING_MED 200
1092 #define PING_HIGH 500
1094 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1095 else if(p < PING_MED)
1096 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1097 else if(p < PING_HIGH)
1099 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1100 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1105 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1108 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1110 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1111 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1114 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1117 if(substring(s, 0, 1) == "[")
1122 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1128 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1129 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1131 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1132 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1137 if(cvar("crypto_aeslevel") >= 2)
1142 if(cvar("crypto_aeslevel") >= 1)
1152 // 2: AES recommended but not available
1153 // 3: AES possible and will be used
1154 // 4: AES recommended and will be used
1160 vector iconSize = '0 0 0';
1161 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1162 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1164 vector iconPos = '0 0 0';
1165 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1166 iconPos_y = (1 - iconSize_y) * 0.5;
1170 if (!(me.seenIPv4 && me.seenIPv6))
1172 iconPos_x += iconSize_x * 0.5;
1174 else if(me.seenIPv4 && me.seenIPv6)
1178 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1180 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1182 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1183 iconPos_x += iconSize_x;
1188 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1189 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1191 iconPos_x += iconSize_x;
1193 if(modname == "Xonotic")
1197 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1198 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1203 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1204 if(draw_PictureSize(n) == '0 0 0')
1205 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1207 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1209 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1211 iconPos_x += iconSize_x;
1213 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1215 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1216 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1218 iconPos_x += iconSize_x;
1226 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1229 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1230 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1233 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1234 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1237 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1238 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1240 // server playercount
1241 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1242 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1245 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1249 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1250 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1252 if(scan == K_ENTER || scan == K_KP_ENTER)
1254 ServerList_Connect_Click(NULL, me);
1257 else if(scan == K_MOUSE2 || scan == K_SPACE)
1261 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1262 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1267 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1271 me.toggleFavorite(me, me.selectedServer);
1272 me.ipAddressBoxFocused = -1;
1277 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1279 else if(!me.controlledTextbox)
1282 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1285 float XonoticServerList_getTotalHeight(entity me) {
1286 float num_normal_rows = me.nItems;
1287 float num_headers = category_draw_count;
1288 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1290 float XonoticServerList_getItemAtPos(entity me, float pos) {
1291 pos = pos / me.itemHeight;
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 (pos >= itempos + me.categoriesHeight + 1)
1297 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1301 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1304 float XonoticServerList_getItemStart(entity me, float item) {
1306 for (i = category_draw_count - 1; i >= 0; --i) {
1307 float itemidx = category_item[i];
1308 float itempos = i * me.categoriesHeight + category_item[i];
1309 if (item >= itemidx + 1)
1310 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1311 if (item >= itemidx)
1312 return itempos * me.itemHeight;
1314 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1315 return item * me.itemHeight;
1317 float XonoticServerList_getItemHeight(entity me, float item) {
1319 for (i = 0; i < category_draw_count; ++i) {
1320 // Matches exactly the headings with increased height.
1321 if (item == category_item[i])
1322 return me.itemHeight * (me.categoriesHeight + 1);
1324 return me.itemHeight;