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 ENDCLASS(XonoticServerList)
61 entity makeXonoticServerList();
63 #ifndef IMPLEMENTATION
64 var float autocvar_menu_slist_categories = TRUE;
65 var float autocvar_menu_slist_categories_onlyifmultiple = TRUE;
66 var float autocvar_menu_slist_purethreshold = 10;
67 var float autocvar_menu_slist_modimpurity = 10;
68 var float autocvar_menu_slist_recommendations = 2;
69 var float autocvar_menu_slist_recommendations_maxping = 150;
70 var float autocvar_menu_slist_recommendations_minfreeslots = 1;
71 var float autocvar_menu_slist_recommendations_minhumans = 1;
72 var float autocvar_menu_slist_recommendations_purethreshold = -1;
73 //var string autocvar_menu_slist_recommended = "76.124.107.5:26004";
75 // server cache fields
76 #define SLIST_FIELDS \
77 SLIST_FIELD(CNAME, "cname") \
78 SLIST_FIELD(PING, "ping") \
79 SLIST_FIELD(GAME, "game") \
80 SLIST_FIELD(MOD, "mod") \
81 SLIST_FIELD(MAP, "map") \
82 SLIST_FIELD(NAME, "name") \
83 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
84 SLIST_FIELD(NUMPLAYERS, "numplayers") \
85 SLIST_FIELD(NUMHUMANS, "numhumans") \
86 SLIST_FIELD(NUMBOTS, "numbots") \
87 SLIST_FIELD(PROTOCOL, "protocol") \
88 SLIST_FIELD(FREESLOTS, "freeslots") \
89 SLIST_FIELD(PLAYERS, "players") \
90 SLIST_FIELD(QCSTATUS, "qcstatus") \
91 SLIST_FIELD(CATEGORY, "category") \
92 SLIST_FIELD(ISFAVORITE, "isfavorite")
94 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
98 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
99 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
100 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
101 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
103 // function declarations
104 entity RetrieveCategoryEnt(float catnum);
106 float IsServerInList(string list, string srv);
107 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
108 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
110 float CheckCategoryOverride(float cat);
111 float CheckCategoryForEntry(float entry);
112 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
114 void RegisterSLCategories();
116 void ServerList_Connect_Click(entity btn, entity me);
117 void ServerList_Categories_Click(entity box, entity me);
118 void ServerList_ShowEmpty_Click(entity box, entity me);
119 void ServerList_ShowFull_Click(entity box, entity me);
120 void ServerList_Filter_Change(entity box, entity me);
121 void ServerList_Favorite_Click(entity btn, entity me);
122 void ServerList_Info_Click(entity btn, entity me);
123 void ServerList_Update_favoriteButton(entity btn, entity me);
125 // fields for category entities
126 #define MAX_CATEGORIES 9
127 #define CATEGORY_FIRST 1
128 entity categories[MAX_CATEGORIES];
129 float category_ent_count;
132 .string cat_enoverride_string;
133 .string cat_dioverride_string;
134 .float cat_enoverride;
135 .float cat_dioverride;
137 // fields for drawing categories
138 float category_name[MAX_CATEGORIES];
139 float category_item[MAX_CATEGORIES];
140 float category_draw_count;
142 #define SLIST_CATEGORIES \
143 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
144 SLIST_CATEGORY(CAT_RECOMMENDED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Recommended"))) \
145 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
146 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
147 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
148 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
149 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
150 SLIST_CATEGORY(CAT_MINSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^MinstaGib Mode"))) \
151 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
153 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
154 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
156 var string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
158 #undef SLIST_CATEGORY
162 #ifdef IMPLEMENTATION
164 void RegisterSLCategories()
167 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
168 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
169 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
171 categories[name - 1] = cat; \
172 cat.classname = "slist_category"; \
173 cat.cat_name = strzone(#name); \
174 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
175 cat.cat_dioverride_string = strzone(dioverride); \
176 cat.cat_string = strzone(str);
178 #undef SLIST_CATEGORY
183 #define PROCESS_OVERRIDE(override_string,override_field) \
184 for(i = 0; i < category_ent_count; ++i) \
186 s = categories[i].override_string; \
187 if((s != "") && (s != categories[i].cat_name)) \
190 for(x = 0; x < category_ent_count; ++x) \
191 { if(categories[x].cat_name == s) { \
197 strunzone(categories[i].override_string); \
198 categories[i].override_field = catnum; \
204 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
206 categories[i].cat_name \
210 strunzone(categories[i].override_string); \
211 categories[i].override_field = 0; \
213 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
214 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
215 #undef PROCESS_OVERRIDE
218 // Supporting Functions
219 entity RetrieveCategoryEnt(float catnum)
221 if((catnum > 0) && (catnum <= category_ent_count))
223 return categories[catnum - 1];
227 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
232 float IsServerInList(string list, string srv)
238 srv = netaddress_resolve(srv, 26000);
241 p = crypto_getidfp(srv);
242 n = tokenize_console(list);
243 for(i = 0; i < n; ++i)
245 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
253 if(srv == netaddress_resolve(argv(i), 26000))
260 float CheckCategoryOverride(float cat)
262 entity catent = RetrieveCategoryEnt(cat);
265 float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
266 if(override) { return override; }
271 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
276 float CheckCategoryForEntry(float entry)
278 string s, k, v, modtype = "";
279 float j, m, impure = 0, freeslots = 0, sflags = 0;
280 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
281 m = tokenizebyseparator(s, ":");
283 for(j = 2; j < m; ++j)
285 if(argv(j) == "") { break; }
286 k = substring(argv(j), 0, 1);
287 v = substring(argv(j), 1, -1);
290 case "P": { impure = stof(v); break; }
291 case "S": { freeslots = stof(v); break; }
292 case "F": { sflags = stof(v); break; }
293 case "M": { modtype = strtolower(v); break; }
297 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
299 // check if this server is favorited
300 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
302 // now check if it's recommended
303 if(autocvar_menu_slist_recommendations)
305 float recommended = 0;
306 if(autocvar_menu_slist_recommendations & 1)
308 if(IsRecommended(gethostcachestring(SLIST_FIELD_CNAME, entry)))
313 if(autocvar_menu_slist_recommendations & 2)
316 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
319 (autocvar_menu_slist_recommendations_purethreshold < 0)
321 (impure <= autocvar_menu_slist_recommendations_purethreshold)
325 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
327 autocvar_menu_slist_recommendations_minhumans
331 gethostcachenumber(SLIST_FIELD_PING, entry)
333 autocvar_menu_slist_recommendations_maxping
340 if(recommended > 0) { return CAT_RECOMMENDED; }
343 // if not favorited or recommended, check modname
344 if(modtype != "xonotic")
348 // old servers which don't report their mod name are considered modified now
349 case "": { return CAT_MODIFIED; }
351 case "xpm": { return CAT_XPM; }
352 case "minstagib": { return CAT_MINSTAGIB; }
353 case "overkill": { return CAT_OVERKILL; }
354 //case "nix": { return CAT_NIX; }
355 //case "newtoys": { return CAT_NEWTOYS; }
357 // "cts" is allowed as compat, xdf is replacement
359 case "xdf": { return CAT_DEFRAG; }
361 default: { dprint(sprintf("Found strange mod type: %s\n", modtype)); return CAT_MODIFIED; }
365 // must be normal or impure server
366 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
369 float CheckItemNumber(float num)
373 if not(category_draw_count) { return num; } // there are no categories to process
375 for(i = 0, n = 1; n <= category_draw_count; ++i, ++n)
377 if(category_item[i] == (num - i)) { return -category_name[i]; }
378 else if(n == category_draw_count) { return (num - n); }
379 else if((num - i) <= category_item[n]) { return (num - n); }
382 // should never hit this point
383 error(sprintf("CheckItemNumber(%d): Function fell through without normal return!\n", num));
387 void XonoticServerList_toggleFavorite(entity me, string srv)
389 string s, s0, s1, s2, srv_resolved, p;
391 srv_resolved = netaddress_resolve(srv, 26000);
392 p = crypto_getidfp(srv_resolved);
393 s = cvar_string("net_slist_favorites");
394 n = tokenize_console(s);
396 for(i = 0; i < n; ++i)
398 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
406 if(srv_resolved != netaddress_resolve(argv(i), 26000))
411 s0 = substring(s, 0, argv_end_index(i - 1));
413 s2 = substring(s, argv_start_index(i + 1), -1);
414 if(s0 != "" && s2 != "")
416 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
417 s = cvar_string("net_slist_favorites");
418 n = tokenize_console(s);
429 cvar_set("net_slist_favorites", strcat(s, s1, p));
431 cvar_set("net_slist_favorites", strcat(s, s1, srv));
434 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
437 void ServerList_Update_favoriteButton(entity btn, entity me)
439 me.favoriteButton.setText(me.favoriteButton,
440 (IsFavorite(me.ipAddressBox.text) ?
441 _("Remove") : _("Bookmark")
446 entity makeXonoticServerList()
449 me = spawnXonoticServerList();
450 me.configureXonoticServerList(me);
453 void XonoticServerList_configureXonoticServerList(entity me)
455 me.configureXonoticListBox(me);
458 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
465 void XonoticServerList_setSelected(entity me, float i)
467 // todo: add logic to skip categories
469 save = me.selectedItem;
470 SUPER(XonoticServerList).setSelected(me, i);
472 if(me.selectedItem == save)
478 //if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != CheckItemNumber(me.nItems))
479 // { error("^1XonoticServerList_setSelected(); ERROR: ^7Host cache viewcount mismatches nItems!\n"); return; } // sorry, it would be wrong
480 // ^ todo: make this work somehow?
482 #define SET_SELECTED_SERVER(cachenum) \
483 if(me.selectedServer) { strunzone(me.selectedServer); } \
484 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, cachenum)); \
485 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer); \
486 me.ipAddressBox.cursorPos = strlen(me.selectedServer); \
487 me.ipAddressBoxFocused = -1; \
490 num = CheckItemNumber(me.selectedItem);
492 if(num >= 0) { SET_SELECTED_SERVER(num); }
493 else if(save > me.selectedItem)
495 if(me.selectedItem == 0) { return; }
498 if(me.lastClickedTime >= me.lastBumpSelectTime)
500 SUPER(XonoticServerList).setSelected(me, me.selectedItem - 1);
501 num = CheckItemNumber(me.selectedItem);
504 me.lastBumpSelectTime = time;
505 SET_SELECTED_SERVER(num);
510 else if(save < me.selectedItem)
512 if(me.selectedItem == me.nItems) { return; }
515 if(me.lastClickedTime >= me.lastBumpSelectTime)
517 SUPER(XonoticServerList).setSelected(me, me.selectedItem + 1);
518 num = CheckItemNumber(me.selectedItem);
521 me.lastBumpSelectTime = time;
522 SET_SELECTED_SERVER(num);
529 void XonoticServerList_refreshServerList(entity me, float mode)
531 //print("refresh of type ", ftos(mode), "\n");
533 if(mode >= REFRESHSERVERLIST_REFILTER)
537 string s, typestr, modstr;
541 m = strstrofs(s, ":", 0);
544 typestr = substring(s, 0, m);
545 s = substring(s, m + 1, strlen(s) - m - 1);
546 while(substring(s, 0, 1) == " ")
547 s = substring(s, 1, strlen(s) - 1);
552 modstr = cvar_string("menu_slist_modfilter");
554 m = SLIST_MASK_AND - 1;
555 resethostcachemasks();
557 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
558 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
561 if(!me.filterShowFull)
563 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
564 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
568 if(!me.filterShowEmpty)
569 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
571 // gametype filtering
573 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
578 if(substring(modstr, 0, 1) == "!")
579 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
581 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
585 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
586 for(i = 0; i < n; ++i)
588 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
590 m = SLIST_MASK_OR - 1;
593 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
594 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
595 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
596 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
600 //listflags |= SLSF_FAVORITES;
601 listflags |= SLSF_CATEGORIES;
602 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
603 sethostcachesort(me.currentSortField, listflags);
607 if(mode >= REFRESHSERVERLIST_ASK)
608 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
610 void XonoticServerList_focusEnter(entity me)
612 if(time < me.nextRefreshTime)
614 //print("sorry, no refresh yet\n");
617 me.nextRefreshTime = time + 10;
618 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
621 void XonoticServerList_draw(entity me)
623 float i, found, owned, num;
625 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
629 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
632 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
636 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
639 if(me.currentSortField == -1)
641 me.setSortOrder(me, SLIST_FIELD_PING, +1);
642 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
644 else if(me.needsRefresh == 1)
646 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
648 else if(me.needsRefresh == 2)
651 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
653 else if(me.needsRefresh == 3)
656 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
659 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
661 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
662 category_draw_count = 0;
664 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
666 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
667 me.nItems = itemcount;
669 //float visible = floor(me.scrollPos / me.itemHeight);
670 // ^ unfortunately no such optimization can be made-- we must process through the
671 // entire list, otherwise there is no way to know which item is first in its category.
675 // binary search method suggested by div
677 float first, middle, last;
679 float catf = 0, catl = 0;
680 for(x = 1; x <= category_ent_count; ++x)
683 last = (itemcount - 1);
684 middle = floor((first + last) / 2);
687 ((catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first)) < x)
689 ((catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last)) >= x)
694 cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
695 if(cat >= x) { last = middle; }
696 else if(cat < x) { first = middle; }
697 if((last - middle) == 1)
699 print(sprintf("highhit: x='%d', dc='%d', first='%d', middle='%d', last='%d', cat='%d'.\n", x, category_draw_count, first, middle, last, cat));
700 category_name[category_draw_count] = x;
701 category_item[category_draw_count] = last;
702 ++category_draw_count;
704 newfirst = last; // already scanned through these, skip 'em
707 middle = floor((first + last) / 2);
712 print(sprintf("lowhit: x='%d', dc='%d', first='%d', middle='%d', last='%d', cat='%d'.\n", x, category_draw_count, first, middle, last, catf));
713 category_name[category_draw_count] = x;
714 category_item[category_draw_count] = first;
715 ++category_draw_count;
717 newfirst = first + 1; // already scanned through these, skip 'em
721 // my binary search method
723 float first, middle, last;
725 for(x = 1; x <= category_ent_count; ++x)
728 last = (itemcount - 1);
729 middle = floor((first + last) / 2);
733 cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
734 if(cat > x) { last = middle - 1; }
737 if(middle == 0 || (gethostcachenumber(SLIST_FIELD_CATEGORY, middle - 1) != x)) // check if middle is the first of its category
739 //print(sprintf("hit: x='%d', dc='%d', first='%d', middle='%d', last='%d', cat='%d'.\n", x, category_draw_count, first, middle, last, cat));
740 category_name[category_draw_count] = cat;
741 category_item[category_draw_count] = middle;
742 ++category_draw_count;
744 newfirst = middle + 1; // already scanned through these, skip 'em
747 else { last = middle - 1; } // nope, try again
749 else { first = middle + 1; }
750 middle = floor((first + last) / 2);
754 // old linear search method
755 /*float cat = 0, i = 0, x = 0;
756 for(i = 0; i < itemcount; ++i) // FIXME this loop is TOTALLY unacceptable (O(servers)). Make it O(categories * log(servers)). Yes, that is possible.
758 cat = gethostcachenumber(SLIST_FIELD_CATEGORY, i);
761 if(category_draw_count == 0)
763 print(sprintf("hit: i='%d', dc='%d', cat='%d'.\n", i, category_draw_count, cat));
764 category_name[category_draw_count] = cat;
765 category_item[category_draw_count] = i;
766 ++category_draw_count;
772 for(x = 0; x < category_draw_count; ++x) { if(cat == category_name[x]) { found = 1; } }
775 print(sprintf("hit: i='%d', dc='%d', cat='%d'.\n", i, category_draw_count, cat));
776 category_name[category_draw_count] = cat;
777 category_item[category_draw_count] = i;
778 ++category_draw_count;
784 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
786 category_name[0] = -1;
787 category_item[0] = -1;
788 category_draw_count = 0;
789 me.nItems = itemcount;
793 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
795 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
796 me.infoButton.disabled = ((me.nItems == 0) || !owned);
797 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
800 if(me.selectedServer)
802 for(i = 0; i < me.nItems; ++i)
804 num = CheckItemNumber(i);
807 if(gethostcachestring(SLIST_FIELD_CNAME, num) == me.selectedServer)
809 if(i != me.selectedItem)
811 me.lastClickedServer = -1;
824 if(me.selectedItem >= me.nItems) { me.selectedItem = me.nItems - 1; }
825 if(me.selectedServer) { strunzone(me.selectedServer); }
827 num = CheckItemNumber(me.selectedItem);
828 if(num >= 0) { me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, num)); }
834 if(me.selectedServer != me.ipAddressBox.text)
836 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
837 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
838 me.ipAddressBoxFocused = -1;
842 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
844 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
845 ServerList_Update_favoriteButton(NULL, me);
846 me.ipAddressBoxFocused = me.ipAddressBox.focused;
849 SUPER(XonoticServerList).draw(me);
851 void ServerList_PingSort_Click(entity btn, entity me)
853 me.setSortOrder(me, SLIST_FIELD_PING, +1);
855 void ServerList_NameSort_Click(entity btn, entity me)
857 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
859 void ServerList_MapSort_Click(entity btn, entity me)
861 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
863 void ServerList_PlayerSort_Click(entity btn, entity me)
865 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
867 void ServerList_TypeSort_Click(entity btn, entity me)
872 m = strstrofs(s, ":", 0);
875 s = substring(s, 0, m);
876 while(substring(s, m+1, 1) == " ") // skip spaces
882 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
884 t = MapInfo_Type_ToString(i);
886 if(t == "") // it repeats (default case)
889 // choose the first one
890 s = MapInfo_Type_ToString(1);
895 // the type was found
896 // choose the next one
897 s = MapInfo_Type_ToString(i * 2);
899 s = MapInfo_Type_ToString(1);
906 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
908 me.controlledTextbox.setText(me.controlledTextbox, s);
909 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
910 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
911 //ServerList_Filter_Change(me.controlledTextbox, me);
913 void ServerList_Filter_Change(entity box, entity me)
916 strunzone(me.filterString);
918 me.filterString = strzone(box.text);
920 me.filterString = string_null;
921 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
923 me.ipAddressBox.setText(me.ipAddressBox, "");
924 me.ipAddressBox.cursorPos = 0;
925 me.ipAddressBoxFocused = -1;
927 void ServerList_Categories_Click(entity box, entity me)
929 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
930 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
932 me.ipAddressBox.setText(me.ipAddressBox, "");
933 me.ipAddressBox.cursorPos = 0;
934 me.ipAddressBoxFocused = -1;
936 void ServerList_ShowEmpty_Click(entity box, entity me)
938 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
939 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
941 me.ipAddressBox.setText(me.ipAddressBox, "");
942 me.ipAddressBox.cursorPos = 0;
943 me.ipAddressBoxFocused = -1;
945 void ServerList_ShowFull_Click(entity box, entity me)
947 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
948 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
950 me.ipAddressBox.setText(me.ipAddressBox, "");
951 me.ipAddressBox.cursorPos = 0;
952 me.ipAddressBoxFocused = -1;
954 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
956 if(me.currentSortField == fld)
957 direction = -me.currentSortOrder;
958 me.currentSortOrder = direction;
959 me.currentSortField = fld;
960 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
961 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
962 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
963 me.sortButton4.forcePressed = 0;
964 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
966 if(me.selectedServer)
967 strunzone(me.selectedServer);
968 me.selectedServer = string_null;
969 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
971 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
973 vector originInLBSpace, sizeInLBSpace;
974 originInLBSpace = eY * (-me.itemHeight);
975 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
977 vector originInDialogSpace, sizeInDialogSpace;
978 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
979 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
981 btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
982 btn.Container_size_x = sizeInDialogSpace_x * theSize;
983 btn.setText(btn, theTitle);
984 btn.onClick = theFunc;
985 btn.onClickEntity = me;
988 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
990 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
992 me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
993 me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
994 me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
996 me.columnIconsOrigin = 0;
997 me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
998 me.columnPingSize = me.realFontSize_x * 3;
999 me.columnMapSize = me.realFontSize_x * 10;
1000 me.columnTypeSize = me.realFontSize_x * 4;
1001 me.columnPlayersSize = me.realFontSize_x * 5;
1002 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
1003 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
1004 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
1005 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
1006 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
1007 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
1009 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
1010 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
1011 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
1012 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
1013 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
1016 f = me.currentSortField;
1019 me.currentSortField = -1;
1020 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
1023 void ServerList_Connect_Click(entity btn, entity me)
1025 localcmd(sprintf("connect %s\n",
1026 ((me.ipAddressBox.text != "") ?
1027 me.ipAddressBox.text : me.selectedServer
1031 void ServerList_Favorite_Click(entity btn, entity me)
1034 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
1037 me.toggleFavorite(me, me.ipAddressBox.text);
1038 me.ipAddressBoxFocused = -1;
1041 void ServerList_Info_Click(entity btn, entity me)
1043 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, CheckItemNumber(me.selectedItem));
1044 DialogOpenButton_Click(me, main.serverInfoDialog);
1046 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
1048 float num = CheckItemNumber(i);
1051 if(num == me.lastClickedServer)
1052 if(time < me.lastClickedTime + 0.3)
1055 ServerList_Connect_Click(NULL, me);
1057 me.lastClickedServer = num;
1058 me.lastClickedTime = time;
1061 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
1063 // layout: Ping, Server name, Map name, NP, TP, MP
1068 float m, pure, freeslots, j, sflags;
1069 string s, typestr, versionstr, k, v, modname;
1071 float item = CheckItemNumber(i);
1072 //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
1076 entity catent = RetrieveCategoryEnt(-item);
1080 eY * me.realUpperMargin
1082 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1094 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1096 s = gethostcachestring(SLIST_FIELD_QCSTATUS, item);
1097 m = tokenizebyseparator(s, ":");
1102 versionstr = argv(1);
1108 for(j = 2; j < m; ++j)
1112 k = substring(argv(j), 0, 1);
1113 v = substring(argv(j), 1, -1);
1117 freeslots = stof(v);
1124 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1126 modname = "Xonotic";
1130 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1131 s = gethostcachestring(SLIST_FIELD_MOD, item);
1133 if(modname == "Xonotic")
1137 // list the mods here on which the pure server check actually works
1138 if(modname != "Xonotic")
1139 if(modname != "MinstaGib")
1140 if(modname != "CTS")
1141 if(modname != "NIX")
1142 if(modname != "NewToys")
1145 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, item) <= 0)
1146 theAlpha = SKINALPHA_SERVERLIST_FULL;
1147 else if(freeslots == 0)
1148 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1149 else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item))
1150 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1154 p = gethostcachenumber(SLIST_FIELD_PING, item);
1156 #define PING_MED 200
1157 #define PING_HIGH 500
1159 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1160 else if(p < PING_MED)
1161 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1162 else if(p < PING_HIGH)
1164 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1165 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1170 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1173 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, item))
1175 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1176 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1179 s = gethostcachestring(SLIST_FIELD_CNAME, item);
1182 if(substring(s, 0, 1) == "[")
1187 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1193 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1194 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1196 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1197 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1202 if(cvar("crypto_aeslevel") >= 2)
1207 if(cvar("crypto_aeslevel") >= 1)
1217 // 2: AES recommended but not available
1218 // 3: AES possible and will be used
1219 // 4: AES recommended and will be used
1225 vector iconSize = '0 0 0';
1226 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1227 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1229 vector iconPos = '0 0 0';
1230 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1231 iconPos_y = (1 - iconSize_y) * 0.5;
1235 if not(me.seenIPv4 && me.seenIPv6)
1237 iconPos_x += iconSize_x * 0.5;
1239 else if(me.seenIPv4 && me.seenIPv6)
1243 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1245 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1247 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1248 iconPos_x += iconSize_x;
1253 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1254 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1256 iconPos_x += iconSize_x;
1258 if(modname == "Xonotic")
1262 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1263 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1268 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1269 if(draw_PictureSize(n) == '0 0 0')
1270 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1272 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1274 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1276 iconPos_x += iconSize_x;
1278 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1280 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1281 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1283 iconPos_x += iconSize_x;
1291 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1294 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, item), me.columnNameSize, 0, me.realFontSize);
1295 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1298 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, item), me.columnMapSize, 0, me.realFontSize);
1299 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1302 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1303 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1305 // server playercount
1306 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, item)));
1307 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1310 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1312 float i = CheckItemNumber(me.selectedItem);
1315 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1316 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1318 me.lastBumpSelectTime = 0;
1320 if(scan == K_ENTER || scan == K_KP_ENTER)
1322 ServerList_Connect_Click(NULL, me);
1325 else if(scan == K_MOUSE2 || scan == K_SPACE)
1327 if((me.nItems != 0) && (i >= 0))
1329 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1330 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1335 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1337 if((me.nItems != 0) && (i >= 0))
1339 me.toggleFavorite(me, me.selectedServer);
1340 me.ipAddressBoxFocused = -1;
1345 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1347 else if(!me.controlledTextbox)
1350 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);