2 CLASS(XonoticServerList) EXTENDS(XonoticListBox)
3 METHOD(XonoticServerList, configureXonoticServerList, void(entity))
4 ATTRIB(XonoticServerList, rowsPerItem, float, 1)
5 METHOD(XonoticServerList, draw, void(entity))
6 METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
7 METHOD(XonoticServerList, clickListBoxItem, void(entity, float, vector))
8 METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
9 METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
10 METHOD(XonoticServerList, toggleFavorite, void(entity, string))
12 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
14 ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
15 ATTRIB(XonoticServerList, realUpperMargin, float, 0)
16 ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
17 ATTRIB(XonoticServerList, columnIconsSize, float, 0)
18 ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
19 ATTRIB(XonoticServerList, columnPingSize, float, 0)
20 ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
21 ATTRIB(XonoticServerList, columnNameSize, float, 0)
22 ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
23 ATTRIB(XonoticServerList, columnMapSize, float, 0)
24 ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
25 ATTRIB(XonoticServerList, columnTypeSize, float, 0)
26 ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
27 ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
29 ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
30 METHOD(XonoticServerList, setSelected, void(entity, float))
31 METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
32 ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
33 ATTRIB(XonoticServerList, filterShowFull, float, 1)
34 ATTRIB(XonoticServerList, filterString, string, string_null)
35 ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
36 ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
37 ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
38 ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
39 METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
40 ATTRIB(XonoticServerList, needsRefresh, float, 1)
41 METHOD(XonoticServerList, focusEnter, void(entity))
42 METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
43 ATTRIB(XonoticServerList, sortButton1, entity, NULL)
44 ATTRIB(XonoticServerList, sortButton2, entity, NULL)
45 ATTRIB(XonoticServerList, sortButton3, entity, NULL)
46 ATTRIB(XonoticServerList, sortButton4, entity, NULL)
47 ATTRIB(XonoticServerList, sortButton5, entity, NULL)
48 ATTRIB(XonoticServerList, connectButton, entity, NULL)
49 ATTRIB(XonoticServerList, infoButton, entity, NULL)
50 ATTRIB(XonoticServerList, currentSortOrder, float, 0)
51 ATTRIB(XonoticServerList, currentSortField, float, -1)
52 ATTRIB(XonoticServerList, lastBumpSelectTime, float, 0)
53 ATTRIB(XonoticServerList, lastClickedServer, float, -1)
54 ATTRIB(XonoticServerList, lastClickedTime, float, 0)
56 ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
58 ATTRIB(XonoticServerList, seenIPv4, float, 0)
59 ATTRIB(XonoticServerList, seenIPv6, float, 0)
60 ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
62 METHOD(XonoticServerList, getTotalHeight, float(entity))
63 METHOD(XonoticServerList, getItemAtPos, float(entity, float))
64 METHOD(XonoticServerList, getItemStart, float(entity, float))
65 METHOD(XonoticServerList, getItemHeight, float(entity, float))
66 ENDCLASS(XonoticServerList)
67 entity makeXonoticServerList();
69 #ifndef IMPLEMENTATION
70 var float autocvar_menu_slist_categories = TRUE;
71 var float autocvar_menu_slist_categories_onlyifmultiple = TRUE;
72 var float autocvar_menu_slist_purethreshold = 10;
73 var float autocvar_menu_slist_modimpurity = 10;
74 var float autocvar_menu_slist_recommendations = 3;
75 var float autocvar_menu_slist_recommendations_maxping = 150;
76 var float autocvar_menu_slist_recommendations_minfreeslots = 1;
77 var float autocvar_menu_slist_recommendations_minhumans = 0;
78 var float autocvar_menu_slist_recommendations_purethreshold = -1;
79 //var string autocvar_menu_slist_recommended = "76.124.107.5:26004";
81 // server cache fields
82 #define SLIST_FIELDS \
83 SLIST_FIELD(CNAME, "cname") \
84 SLIST_FIELD(PING, "ping") \
85 SLIST_FIELD(GAME, "game") \
86 SLIST_FIELD(MOD, "mod") \
87 SLIST_FIELD(MAP, "map") \
88 SLIST_FIELD(NAME, "name") \
89 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
90 SLIST_FIELD(NUMPLAYERS, "numplayers") \
91 SLIST_FIELD(NUMHUMANS, "numhumans") \
92 SLIST_FIELD(NUMBOTS, "numbots") \
93 SLIST_FIELD(PROTOCOL, "protocol") \
94 SLIST_FIELD(FREESLOTS, "freeslots") \
95 SLIST_FIELD(PLAYERS, "players") \
96 SLIST_FIELD(QCSTATUS, "qcstatus") \
97 SLIST_FIELD(CATEGORY, "category") \
98 SLIST_FIELD(ISFAVORITE, "isfavorite")
100 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
104 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
105 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
106 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
107 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
109 // function declarations
110 entity RetrieveCategoryEnt(float catnum);
112 float IsServerInList(string list, string srv);
113 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
114 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
115 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
117 float CheckCategoryOverride(float cat);
118 float CheckCategoryForEntry(float entry);
119 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
121 void RegisterSLCategories();
123 void ServerList_Connect_Click(entity btn, entity me);
124 void ServerList_Categories_Click(entity box, entity me);
125 void ServerList_ShowEmpty_Click(entity box, entity me);
126 void ServerList_ShowFull_Click(entity box, entity me);
127 void ServerList_Filter_Change(entity box, entity me);
128 void ServerList_Favorite_Click(entity btn, entity me);
129 void ServerList_Info_Click(entity btn, entity me);
130 void ServerList_Update_favoriteButton(entity btn, entity me);
132 // fields for category entities
133 #define MAX_CATEGORIES 9
134 #define CATEGORY_FIRST 1
135 entity categories[MAX_CATEGORIES];
136 float category_ent_count;
139 .string cat_enoverride_string;
140 .string cat_dioverride_string;
141 .float cat_enoverride;
142 .float cat_dioverride;
144 // fields for drawing categories
145 float category_name[MAX_CATEGORIES];
146 float category_item[MAX_CATEGORIES];
147 float category_draw_count;
149 #define SLIST_CATEGORIES \
150 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
151 SLIST_CATEGORY(CAT_RECOMMENDED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Recommended"))) \
152 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
153 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
154 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
155 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
156 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
157 SLIST_CATEGORY(CAT_MINSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^MinstaGib Mode"))) \
158 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
160 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
161 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
163 var string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
165 #undef SLIST_CATEGORY
169 #ifdef IMPLEMENTATION
171 void RegisterSLCategories()
174 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
175 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
176 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
178 categories[name - 1] = cat; \
179 cat.classname = "slist_category"; \
180 cat.cat_name = strzone(#name); \
181 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
182 cat.cat_dioverride_string = strzone(dioverride); \
183 cat.cat_string = strzone(str);
185 #undef SLIST_CATEGORY
190 #define PROCESS_OVERRIDE(override_string,override_field) \
191 for(i = 0; i < category_ent_count; ++i) \
193 s = categories[i].override_string; \
194 if((s != "") && (s != categories[i].cat_name)) \
197 for(x = 0; x < category_ent_count; ++x) \
198 { if(categories[x].cat_name == s) { \
204 strunzone(categories[i].override_string); \
205 categories[i].override_field = catnum; \
211 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
213 categories[i].cat_name \
217 strunzone(categories[i].override_string); \
218 categories[i].override_field = 0; \
220 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
221 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
222 #undef PROCESS_OVERRIDE
225 // Supporting Functions
226 entity RetrieveCategoryEnt(float catnum)
228 if((catnum > 0) && (catnum <= category_ent_count))
230 return categories[catnum - 1];
234 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
239 float IsServerInList(string list, string srv)
245 srv = netaddress_resolve(srv, 26000);
248 p = crypto_getidfp(srv);
249 n = tokenize_console(list);
250 for(i = 0; i < n; ++i)
252 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
260 if(srv == netaddress_resolve(argv(i), 26000))
267 float CheckCategoryOverride(float cat)
269 entity catent = RetrieveCategoryEnt(cat);
272 float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
273 if(override) { return override; }
278 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
283 float CheckCategoryForEntry(float entry)
285 string s, k, v, modtype = "";
286 float j, m, impure = 0, freeslots = 0, sflags = 0;
287 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
288 m = tokenizebyseparator(s, ":");
290 for(j = 2; j < m; ++j)
292 if(argv(j) == "") { break; }
293 k = substring(argv(j), 0, 1);
294 v = substring(argv(j), 1, -1);
297 case "P": { impure = stof(v); break; }
298 case "S": { freeslots = stof(v); break; }
299 case "F": { sflags = stof(v); break; }
300 case "M": { modtype = strtolower(v); break; }
304 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
306 // check if this server is favorited
307 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
309 // now check if it's recommended
310 if(autocvar_menu_slist_recommendations)
312 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
314 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
317 float recommended = 0;
318 if(autocvar_menu_slist_recommendations & 1)
320 if(IsRecommended(cname)) { ++recommended; }
321 else { --recommended; }
323 if(autocvar_menu_slist_recommendations & 2)
326 ///// check for minimum free slots
327 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
329 && // check for purity requirement
331 (autocvar_menu_slist_recommendations_purethreshold < 0)
333 (impure <= autocvar_menu_slist_recommendations_purethreshold)
336 && // check for minimum amount of humans
338 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
340 autocvar_menu_slist_recommendations_minhumans
343 && // check for maximum latency
345 gethostcachenumber(SLIST_FIELD_PING, entry)
347 autocvar_menu_slist_recommendations_maxping
354 if(recommended > 0) { return CAT_RECOMMENDED; }
358 // if not favorited or recommended, check modname
359 if(modtype != "xonotic")
363 // old servers which don't report their mod name are considered modified now
364 case "": { return CAT_MODIFIED; }
366 case "xpm": { return CAT_XPM; }
367 case "minstagib": { return CAT_MINSTAGIB; }
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: { dprint(sprintf("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;
388 srv_resolved = netaddress_resolve(srv, 26000);
389 p = crypto_getidfp(srv_resolved);
390 s = cvar_string("net_slist_favorites");
391 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 = spawnXonoticServerList();
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, float i)
465 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, float mode)
486 //print("refresh of type ", ftos(mode), "\n");
488 if(mode >= REFRESHSERVERLIST_REFILTER)
492 string s, typestr, modstr;
496 m = strstrofs(s, ":", 0);
499 typestr = substring(s, 0, m);
500 s = substring(s, m + 1, strlen(s) - m - 1);
501 while(substring(s, 0, 1) == " ")
502 s = substring(s, 1, strlen(s) - 1);
507 modstr = cvar_string("menu_slist_modfilter");
509 m = SLIST_MASK_AND - 1;
510 resethostcachemasks();
512 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
513 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
516 if(!me.filterShowFull)
518 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
519 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
523 if(!me.filterShowEmpty)
524 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
526 // gametype filtering
528 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
533 if(substring(modstr, 0, 1) == "!")
534 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
536 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
540 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
541 for(i = 0; i < n; ++i)
543 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
545 m = SLIST_MASK_OR - 1;
548 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
549 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
550 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
551 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
555 //listflags |= SLSF_FAVORITES;
556 listflags |= SLSF_CATEGORIES;
557 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
558 sethostcachesort(me.currentSortField, listflags);
562 if(mode >= REFRESHSERVERLIST_ASK)
563 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
565 void XonoticServerList_focusEnter(entity me)
567 if(time < me.nextRefreshTime)
569 //print("sorry, no refresh yet\n");
572 me.nextRefreshTime = time + 10;
573 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
576 void XonoticServerList_draw(entity me)
578 float i, found, owned;
580 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
584 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
587 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
591 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
594 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
598 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
601 if(me.currentSortField == -1)
603 me.setSortOrder(me, SLIST_FIELD_PING, +1);
604 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
606 else if(me.needsRefresh == 1)
608 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
610 else if(me.needsRefresh == 2)
613 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
615 else if(me.needsRefresh == 3)
618 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
621 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
623 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
624 category_draw_count = 0;
626 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
628 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
629 me.nItems = itemcount;
631 //float visible = floor(me.scrollPos / me.itemHeight);
632 // ^ unfortunately no such optimization can be made-- we must process through the
633 // entire list, otherwise there is no way to know which item is first in its category.
635 // binary search method suggested by div
638 for(x = 1; x <= category_ent_count; ++x) {
640 float last = (itemcount - 1);
645 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
646 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
648 // The first one is already > x.
649 // Therefore, category x does not exist.
650 // Higher numbered categories do exist though.
651 } else if (catl < x) {
652 // The last one is < x.
653 // Thus this category - and any following -
656 } else if (catf == x) {
657 // Starts at first. This breaks the loop
658 // invariant in the binary search and thus has
659 // to be handled separately.
660 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
661 error("Category mismatch I");
663 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
664 error("Category mismatch II");
665 category_name[category_draw_count] = x;
666 category_item[category_draw_count] = first;
667 ++category_draw_count;
670 // At this point, catf <= x < catl, thus
671 // catf < catl, thus first < last.
674 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
675 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
678 while (last - first > 1) {
679 float middle = floor((first + last) / 2);
680 // By loop condition, middle != first && middle != last.
681 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
691 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
692 error("Category mismatch III");
694 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
695 error("Category mismatch IV");
696 category_name[category_draw_count] = x;
697 category_item[category_draw_count] = last;
698 ++category_draw_count;
699 begin = last + 1; // already scanned through these, skip 'em
702 begin = last; // already scanned through these, skip 'em
705 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
707 category_name[0] = -1;
708 category_item[0] = -1;
709 category_draw_count = 0;
710 me.nItems = itemcount;
713 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
715 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
716 me.infoButton.disabled = ((me.nItems == 0) || !owned);
717 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
720 if(me.selectedServer)
722 for(i = 0; i < me.nItems; ++i)
724 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
726 if(i != me.selectedItem)
728 me.lastClickedServer = -1;
740 if(me.selectedItem >= me.nItems)
741 me.selectedItem = me.nItems - 1;
742 if(me.selectedServer)
743 strunzone(me.selectedServer);
744 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
750 if(me.selectedServer != me.ipAddressBox.text)
752 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
753 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
754 me.ipAddressBoxFocused = -1;
758 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
760 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
761 ServerList_Update_favoriteButton(NULL, me);
762 me.ipAddressBoxFocused = me.ipAddressBox.focused;
765 SUPER(XonoticServerList).draw(me);
767 void ServerList_PingSort_Click(entity btn, entity me)
769 me.setSortOrder(me, SLIST_FIELD_PING, +1);
771 void ServerList_NameSort_Click(entity btn, entity me)
773 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
775 void ServerList_MapSort_Click(entity btn, entity me)
777 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
779 void ServerList_PlayerSort_Click(entity btn, entity me)
781 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
783 void ServerList_TypeSort_Click(entity btn, entity me)
788 m = strstrofs(s, ":", 0);
791 s = substring(s, 0, m);
792 while(substring(s, m+1, 1) == " ") // skip spaces
798 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
800 t = MapInfo_Type_ToString(i);
802 if(t == "") // it repeats (default case)
805 // choose the first one
806 s = MapInfo_Type_ToString(1);
811 // the type was found
812 // choose the next one
813 s = MapInfo_Type_ToString(i * 2);
815 s = MapInfo_Type_ToString(1);
822 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
824 me.controlledTextbox.setText(me.controlledTextbox, s);
825 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
826 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
827 //ServerList_Filter_Change(me.controlledTextbox, me);
829 void ServerList_Filter_Change(entity box, entity me)
832 strunzone(me.filterString);
834 me.filterString = strzone(box.text);
836 me.filterString = string_null;
837 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
839 me.ipAddressBox.setText(me.ipAddressBox, "");
840 me.ipAddressBox.cursorPos = 0;
841 me.ipAddressBoxFocused = -1;
843 void ServerList_Categories_Click(entity box, entity me)
845 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
846 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
848 me.ipAddressBox.setText(me.ipAddressBox, "");
849 me.ipAddressBox.cursorPos = 0;
850 me.ipAddressBoxFocused = -1;
852 void ServerList_ShowEmpty_Click(entity box, entity me)
854 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
855 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
857 me.ipAddressBox.setText(me.ipAddressBox, "");
858 me.ipAddressBox.cursorPos = 0;
859 me.ipAddressBoxFocused = -1;
861 void ServerList_ShowFull_Click(entity box, entity me)
863 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
864 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
866 me.ipAddressBox.setText(me.ipAddressBox, "");
867 me.ipAddressBox.cursorPos = 0;
868 me.ipAddressBoxFocused = -1;
870 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
872 if(me.currentSortField == fld)
873 direction = -me.currentSortOrder;
874 me.currentSortOrder = direction;
875 me.currentSortField = fld;
876 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
877 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
878 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
879 me.sortButton4.forcePressed = 0;
880 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
882 if(me.selectedServer)
883 strunzone(me.selectedServer);
884 me.selectedServer = string_null;
885 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
887 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
889 vector originInLBSpace, sizeInLBSpace;
890 originInLBSpace = eY * (-me.itemHeight);
891 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
893 vector originInDialogSpace, sizeInDialogSpace;
894 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
895 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
897 btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
898 btn.Container_size_x = sizeInDialogSpace_x * theSize;
899 btn.setText(btn, theTitle);
900 btn.onClick = theFunc;
901 btn.onClickEntity = me;
904 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
906 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
908 me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
909 me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
910 me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
912 me.columnIconsOrigin = 0;
913 me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
914 me.columnPingSize = me.realFontSize_x * 3;
915 me.columnMapSize = me.realFontSize_x * 10;
916 me.columnTypeSize = me.realFontSize_x * 4;
917 me.columnPlayersSize = me.realFontSize_x * 5;
918 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
919 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
920 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
921 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
922 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
923 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
925 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
926 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
927 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
928 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
929 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
932 f = me.currentSortField;
935 me.currentSortField = -1;
936 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
939 void ServerList_Connect_Click(entity btn, entity me)
941 localcmd(sprintf("connect %s\n",
942 ((me.ipAddressBox.text != "") ?
943 me.ipAddressBox.text : me.selectedServer
947 void ServerList_Favorite_Click(entity btn, entity me)
950 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
953 me.toggleFavorite(me, me.ipAddressBox.text);
954 me.ipAddressBoxFocused = -1;
957 void ServerList_Info_Click(entity btn, entity me)
960 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
961 DialogOpenButton_Click(me, main.serverInfoDialog);
963 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
965 if(i == me.lastClickedServer)
966 if(time < me.lastClickedTime + 0.3)
969 ServerList_Connect_Click(NULL, me);
971 me.lastClickedServer = i;
972 me.lastClickedTime = time;
974 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
976 // layout: Ping, Server name, Map name, NP, TP, MP
981 float m, pure, freeslots, j, sflags;
982 string s, typestr, versionstr, k, v, modname;
984 //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
986 vector oldscale = draw_scale;
987 vector oldshift = draw_shift;
988 #define SET_YRANGE(start,end) \
989 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
990 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
992 for (j = 0; j < category_draw_count; ++j) {
993 // Matches exactly the headings with increased height.
994 if (i == category_item[j])
998 if (j < category_draw_count)
1000 entity catent = RetrieveCategoryEnt(category_name[j]);
1004 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1005 me.categoriesHeight / (me.categoriesHeight + 1)
1008 eY * me.realUpperMargin
1011 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1014 eX * (me.columnNameOrigin),
1015 strcat(catent.cat_string, ":"),
1022 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1027 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1029 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1030 m = tokenizebyseparator(s, ":");
1035 versionstr = argv(1);
1041 for(j = 2; j < m; ++j)
1045 k = substring(argv(j), 0, 1);
1046 v = substring(argv(j), 1, -1);
1050 freeslots = stof(v);
1057 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1059 modname = "Xonotic";
1063 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1064 s = gethostcachestring(SLIST_FIELD_MOD, i);
1066 if(modname == "Xonotic")
1070 // list the mods here on which the pure server check actually works
1071 if(modname != "Xonotic")
1072 if(modname != "MinstaGib")
1073 if(modname != "CTS")
1074 if(modname != "NIX")
1075 if(modname != "NewToys")
1078 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1079 theAlpha = SKINALPHA_SERVERLIST_FULL;
1080 else if(freeslots == 0)
1081 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1082 else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1083 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1087 p = gethostcachenumber(SLIST_FIELD_PING, i);
1089 #define PING_MED 200
1090 #define PING_HIGH 500
1092 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1093 else if(p < PING_MED)
1094 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1095 else if(p < PING_HIGH)
1097 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1098 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1103 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1106 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1108 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1109 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1112 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1115 if(substring(s, 0, 1) == "[")
1120 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1126 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1127 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1129 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1130 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1135 if(cvar("crypto_aeslevel") >= 2)
1140 if(cvar("crypto_aeslevel") >= 1)
1150 // 2: AES recommended but not available
1151 // 3: AES possible and will be used
1152 // 4: AES recommended and will be used
1158 vector iconSize = '0 0 0';
1159 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1160 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1162 vector iconPos = '0 0 0';
1163 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1164 iconPos_y = (1 - iconSize_y) * 0.5;
1168 if not(me.seenIPv4 && me.seenIPv6)
1170 iconPos_x += iconSize_x * 0.5;
1172 else if(me.seenIPv4 && me.seenIPv6)
1176 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1178 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1180 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1181 iconPos_x += iconSize_x;
1186 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1187 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1189 iconPos_x += iconSize_x;
1191 if(modname == "Xonotic")
1195 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1196 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1201 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1202 if(draw_PictureSize(n) == '0 0 0')
1203 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1205 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1207 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1209 iconPos_x += iconSize_x;
1211 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1213 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1214 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1216 iconPos_x += iconSize_x;
1224 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1227 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1228 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1231 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1232 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1235 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1236 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1238 // server playercount
1239 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1240 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1243 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1245 float i = me.selectedItem;
1248 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1249 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1251 me.lastBumpSelectTime = 0;
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 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1263 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1268 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1272 me.toggleFavorite(me, me.selectedServer);
1273 me.ipAddressBoxFocused = -1;
1278 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1280 else if(!me.controlledTextbox)
1283 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1286 float XonoticServerList_getTotalHeight(entity me) {
1287 float num_normal_rows = me.nItems;
1288 float num_headers = category_draw_count;
1289 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1291 float XonoticServerList_getItemAtPos(entity me, float pos) {
1292 pos = pos / me.itemHeight;
1294 for (i = category_draw_count - 1; i >= 0; --i) {
1295 float itemidx = category_item[i];
1296 float itempos = i * me.categoriesHeight + category_item[i];
1297 if (pos >= itempos + me.categoriesHeight + 1)
1298 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1302 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1305 float XonoticServerList_getItemStart(entity me, float item) {
1307 for (i = category_draw_count - 1; i >= 0; --i) {
1308 float itemidx = category_item[i];
1309 float itempos = i * me.categoriesHeight + category_item[i];
1310 if (item >= itemidx + 1)
1311 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1312 if (item >= itemidx)
1313 return itempos * me.itemHeight;
1315 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1316 return item * me.itemHeight;
1318 float XonoticServerList_getItemHeight(entity me, float item) {
1320 for (i = 0; i < category_draw_count; ++i) {
1321 // Matches exactly the headings with increased height.
1322 if (item == category_item[i])
1323 return me.itemHeight * (me.categoriesHeight + 1);
1325 return me.itemHeight;