4 CLASS(XonoticServerList, XonoticListBox)
5 METHOD(XonoticServerList, configureXonoticServerList, void(entity))
6 ATTRIB(XonoticServerList, rowsPerItem, float, 1)
7 METHOD(XonoticServerList, draw, void(entity))
8 METHOD(XonoticServerList, drawListBoxItem, void(entity, int, vector, bool, bool))
9 METHOD(XonoticServerList, doubleClickListBoxItem, void(entity, float, vector))
10 METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
11 METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
12 METHOD(XonoticServerList, toggleFavorite, void(entity, string))
14 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
16 ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
17 ATTRIB(XonoticServerList, realUpperMargin, float, 0)
18 ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
19 ATTRIB(XonoticServerList, columnIconsSize, float, 0)
20 ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
21 ATTRIB(XonoticServerList, columnPingSize, float, 0)
22 ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
23 ATTRIB(XonoticServerList, columnNameSize, float, 0)
24 ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
25 ATTRIB(XonoticServerList, columnMapSize, float, 0)
26 ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
27 ATTRIB(XonoticServerList, columnTypeSize, float, 0)
28 ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
29 ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
31 ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
32 METHOD(XonoticServerList, setSelected, void(entity, float))
33 METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
34 ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
35 ATTRIB(XonoticServerList, filterShowFull, float, 1)
36 ATTRIB(XonoticServerList, filterString, string, string_null)
37 ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
38 ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
39 ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
40 ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
41 METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
42 ATTRIB(XonoticServerList, needsRefresh, float, 1)
43 METHOD(XonoticServerList, focusEnter, void(entity))
44 METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
45 ATTRIB(XonoticServerList, sortButton1, entity, NULL)
46 ATTRIB(XonoticServerList, sortButton2, entity, NULL)
47 ATTRIB(XonoticServerList, sortButton3, entity, NULL)
48 ATTRIB(XonoticServerList, sortButton4, entity, NULL)
49 ATTRIB(XonoticServerList, sortButton5, entity, NULL)
50 ATTRIB(XonoticServerList, connectButton, entity, NULL)
51 ATTRIB(XonoticServerList, infoButton, entity, NULL)
52 ATTRIB(XonoticServerList, currentSortOrder, float, 0)
53 ATTRIB(XonoticServerList, currentSortField, float, -1)
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 const int MAX_CATEGORIES = 9;
132 const int CATEGORY_FIRST = 1;
133 entity categories[MAX_CATEGORIES];
134 int 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 int category_name[MAX_CATEGORIES];
144 int category_item[MAX_CATEGORIES];
145 int 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 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(int 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 bool 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 int CheckCategoryOverride(int cat)
267 entity catent = RetrieveCategoryEnt(cat);
270 int 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 int CheckCategoryForEntry(int entry)
283 string s, k, v, modtype = "";
284 int 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;
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);
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 = NEW(XonoticServerList);
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, int i)
463 //int save = me.selectedItem;
464 SUPER(XonoticServerList).setSelected(me, i);
466 if(me.selectedItem == save)
471 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
472 return; // sorry, it would be wrong
474 if(me.selectedServer)
475 strunzone(me.selectedServer);
476 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
478 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
479 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
480 me.ipAddressBoxFocused = -1;
482 void XonoticServerList_refreshServerList(entity me, int mode)
484 //print("refresh of type ", ftos(mode), "\n");
486 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 SUPER(XonoticServerList).focusEnter(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)
579 bool found = false, owned;
581 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
585 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
588 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
592 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
595 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
599 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
602 if(me.currentSortField == -1)
604 me.setSortOrder(me, SLIST_FIELD_PING, +1);
605 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
607 else if(me.needsRefresh == 1)
609 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
611 else if(me.needsRefresh == 2)
614 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
616 else if(me.needsRefresh == 3)
619 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
622 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
624 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
625 category_draw_count = 0;
627 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
629 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
630 me.nItems = itemcount;
632 //float visible = floor(me.scrollPos / me.itemHeight);
633 // ^ unfortunately no such optimization can be made-- we must process through the
634 // entire list, otherwise there is no way to know which item is first in its category.
636 // binary search method suggested by div
639 for(x = 1; x <= category_ent_count; ++x) {
641 float last = (itemcount - 1);
646 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
647 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
649 // The first one is already > x.
650 // Therefore, category x does not exist.
651 // Higher numbered categories do exist though.
652 } else if (catl < x) {
653 // The last one is < x.
654 // Thus this category - and any following -
657 } else if (catf == x) {
658 // Starts at first. This breaks the loop
659 // invariant in the binary search and thus has
660 // to be handled separately.
661 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
662 error("Category mismatch I");
664 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
665 error("Category mismatch II");
666 category_name[category_draw_count] = x;
667 category_item[category_draw_count] = first;
668 ++category_draw_count;
671 // At this point, catf <= x < catl, thus
672 // catf < catl, thus first < last.
675 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
676 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
679 while (last - first > 1) {
680 float middle = floor((first + last) / 2);
681 // By loop condition, middle != first && middle != last.
682 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
692 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
693 error("Category mismatch III");
695 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
696 error("Category mismatch IV");
697 category_name[category_draw_count] = x;
698 category_item[category_draw_count] = last;
699 ++category_draw_count;
700 begin = last + 1; // already scanned through these, skip 'em
703 begin = last; // already scanned through these, skip 'em
706 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
708 category_name[0] = -1;
709 category_item[0] = -1;
710 category_draw_count = 0;
711 me.nItems = itemcount;
714 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
716 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
717 me.infoButton.disabled = ((me.nItems == 0) || !owned);
718 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)
736 if(me.selectedItem >= me.nItems)
737 me.selectedItem = me.nItems - 1;
738 if(me.selectedServer)
739 strunzone(me.selectedServer);
740 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
746 if(me.selectedServer != me.ipAddressBox.text)
748 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
749 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
750 me.ipAddressBoxFocused = -1;
754 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
756 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
757 ServerList_Update_favoriteButton(NULL, me);
758 me.ipAddressBoxFocused = me.ipAddressBox.focused;
761 SUPER(XonoticServerList).draw(me);
763 void ServerList_PingSort_Click(entity btn, entity me)
765 me.setSortOrder(me, SLIST_FIELD_PING, +1);
767 void ServerList_NameSort_Click(entity btn, entity me)
769 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
771 void ServerList_MapSort_Click(entity btn, entity me)
773 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
775 void ServerList_PlayerSort_Click(entity btn, entity me)
777 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
779 void ServerList_TypeSort_Click(entity btn, entity me)
784 m = strstrofs(s, ":", 0);
787 s = substring(s, 0, m);
788 while(substring(s, m+1, 1) == " ") // skip spaces
794 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
796 t = MapInfo_Type_ToString(i);
798 if(t == "") // it repeats (default case)
801 // choose the first one
802 s = MapInfo_Type_ToString(1);
807 // the type was found
808 // choose the next one
809 s = MapInfo_Type_ToString(i * 2);
811 s = MapInfo_Type_ToString(1);
818 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
820 me.controlledTextbox.setText(me.controlledTextbox, s);
821 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
822 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
823 //ServerList_Filter_Change(me.controlledTextbox, me);
825 void ServerList_Filter_Change(entity box, entity me)
828 strunzone(me.filterString);
830 me.filterString = strzone(box.text);
832 me.filterString = string_null;
833 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
835 me.ipAddressBox.setText(me.ipAddressBox, "");
836 me.ipAddressBox.cursorPos = 0;
837 me.ipAddressBoxFocused = -1;
839 void ServerList_Categories_Click(entity box, entity me)
841 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
842 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
844 me.ipAddressBox.setText(me.ipAddressBox, "");
845 me.ipAddressBox.cursorPos = 0;
846 me.ipAddressBoxFocused = -1;
848 void ServerList_ShowEmpty_Click(entity box, entity me)
850 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
851 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
853 me.ipAddressBox.setText(me.ipAddressBox, "");
854 me.ipAddressBox.cursorPos = 0;
855 me.ipAddressBoxFocused = -1;
857 void ServerList_ShowFull_Click(entity box, entity me)
859 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
860 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
862 me.ipAddressBox.setText(me.ipAddressBox, "");
863 me.ipAddressBox.cursorPos = 0;
864 me.ipAddressBoxFocused = -1;
866 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
868 if(me.currentSortField == fld)
869 direction = -me.currentSortOrder;
870 me.currentSortOrder = direction;
871 me.currentSortField = fld;
872 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
873 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
874 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
875 me.sortButton4.forcePressed = 0;
876 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
878 if(me.selectedServer)
879 strunzone(me.selectedServer);
880 me.selectedServer = string_null;
881 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
883 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
885 vector originInLBSpace, sizeInLBSpace;
886 originInLBSpace = eY * (-me.itemHeight);
887 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
889 vector originInDialogSpace, sizeInDialogSpace;
890 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
891 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
893 btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
894 btn.Container_size_x = sizeInDialogSpace.x * theSize;
895 btn.setText(btn, theTitle);
896 btn.onClick = theFunc;
897 btn.onClickEntity = me;
900 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
902 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
904 me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
905 me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
906 me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
908 me.columnIconsOrigin = 0;
909 me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
910 me.columnPingSize = me.realFontSize.x * 3;
911 me.columnMapSize = me.realFontSize.x * 10;
912 me.columnTypeSize = me.realFontSize.x * 4;
913 me.columnPlayersSize = me.realFontSize.x * 5;
914 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
915 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
916 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
917 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
918 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
919 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
921 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
922 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
923 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
924 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
925 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
927 int f = me.currentSortField;
930 me.currentSortField = -1;
931 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
934 void ServerList_Connect_Click(entity btn, entity me)
936 localcmd(sprintf("connect %s\n",
937 ((me.ipAddressBox.text != "") ?
938 me.ipAddressBox.text : me.selectedServer
942 void ServerList_Favorite_Click(entity btn, entity me)
945 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
948 m_play_click_sound(MENU_SOUND_SELECT);
949 me.toggleFavorite(me, me.ipAddressBox.text);
950 me.ipAddressBoxFocused = -1;
953 void ServerList_Info_Click(entity btn, entity me)
956 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
958 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
959 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
960 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
962 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
964 ServerList_Connect_Click(NULL, me);
966 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected, bool isFocused)
968 // layout: Ping, Server name, Map name, NP, TP, MP
975 int freeslots = -1, sflags = -1, j, m;
976 string s, typestr, versionstr, k, v, modname;
978 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
980 vector oldscale = draw_scale;
981 vector oldshift = draw_shift;
982 #define SET_YRANGE(start,end) \
983 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
984 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
986 for (j = 0; j < category_draw_count; ++j) {
987 // Matches exactly the headings with increased height.
988 if (i == category_item[j])
992 if (j < category_draw_count)
994 entity catent = RetrieveCategoryEnt(category_name[j]);
998 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
999 me.categoriesHeight / (me.categoriesHeight + 1)
1002 eY * me.realUpperMargin
1005 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1008 eX * (me.columnNameOrigin),
1009 strcat(catent.cat_string, ":"),
1012 SKINCOLOR_SERVERLIST_CATEGORY,
1013 SKINALPHA_SERVERLIST_CATEGORY,
1016 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1021 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1024 me.focusedItemAlpha = getFadedAlpha(me.focusedItemAlpha, SKINALPHA_LISTBOX_FOCUSED, SKINFADEALPHA_LISTBOX_FOCUSED);
1025 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_FOCUSED, me.focusedItemAlpha);
1028 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1029 m = tokenizebyseparator(s, ":");
1034 versionstr = argv(1);
1038 for(j = 2; j < m; ++j)
1042 k = substring(argv(j), 0, 1);
1043 v = substring(argv(j), 1, -1);
1047 freeslots = stof(v);
1054 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1056 modname = "Xonotic";
1060 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1061 s = gethostcachestring(SLIST_FIELD_MOD, i);
1063 if(modname == "Xonotic")
1067 // list the mods here on which the pure server check actually works
1068 if(modname != "Xonotic")
1069 if(modname != "InstaGib" || modname != "MinstaGib")
1070 if(modname != "CTS")
1071 if(modname != "NIX")
1072 if(modname != "NewToys")
1075 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1076 theAlpha = SKINALPHA_SERVERLIST_FULL;
1077 else if(freeslots == 0)
1078 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1079 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1080 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1084 p = gethostcachenumber(SLIST_FIELD_PING, i);
1085 const int PING_LOW = 75;
1086 const int PING_MED = 200;
1087 const int PING_HIGH = 500;
1089 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1090 else if(p < PING_MED)
1091 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1092 else if(p < PING_HIGH)
1094 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1095 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1100 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1103 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1105 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1106 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1109 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1111 isv4 = isv6 = false;
1112 if(substring(s, 0, 1) == "[")
1117 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1123 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1124 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1126 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1127 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1132 if(cvar("crypto_aeslevel") >= 2)
1137 if(cvar("crypto_aeslevel") >= 1)
1147 // 2: AES recommended but not available
1148 // 3: AES possible and will be used
1149 // 4: AES recommended and will be used
1155 vector iconSize = '0 0 0';
1156 iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1157 iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1159 vector iconPos = '0 0 0';
1160 iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1161 iconPos_y = (1 - iconSize.y) * 0.5;
1165 if (!(me.seenIPv4 && me.seenIPv6))
1167 iconPos.x += iconSize.x * 0.5;
1169 else if(me.seenIPv4 && me.seenIPv6)
1173 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1175 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1177 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1178 iconPos.x += iconSize.x;
1183 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1184 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1186 iconPos.x += iconSize.x;
1188 if(modname == "Xonotic")
1192 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1193 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1198 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1199 if(draw_PictureSize(n) == '0 0 0')
1200 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1202 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1204 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1206 iconPos.x += iconSize.x;
1208 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1210 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1211 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1213 iconPos.x += iconSize.x;
1221 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1224 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1225 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1228 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1229 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1232 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1233 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1235 // server playercount
1236 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1237 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1240 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1244 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1245 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1247 if(scan == K_ENTER || scan == K_KP_ENTER)
1249 ServerList_Connect_Click(NULL, me);
1252 else if(scan == K_MOUSE2 || scan == K_SPACE)
1256 m_play_click_sound(MENU_SOUND_OPEN);
1257 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1258 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1263 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1267 me.toggleFavorite(me, me.selectedServer);
1268 me.ipAddressBoxFocused = -1;
1273 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1275 else if(!me.controlledTextbox)
1278 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1281 float XonoticServerList_getTotalHeight(entity me)
1283 float num_normal_rows = me.nItems;
1284 int num_headers = category_draw_count;
1285 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1287 int XonoticServerList_getItemAtPos(entity me, float pos)
1289 pos = pos / me.itemHeight;
1291 for (i = category_draw_count - 1; i >= 0; --i) {
1292 int itemidx = category_item[i];
1293 float itempos = i * me.categoriesHeight + category_item[i];
1294 if (pos >= itempos + me.categoriesHeight + 1)
1295 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1299 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1302 float XonoticServerList_getItemStart(entity me, int item)
1305 for (i = category_draw_count - 1; i >= 0; --i) {
1306 int itemidx = category_item[i];
1307 float itempos = i * me.categoriesHeight + category_item[i];
1308 if (item >= itemidx + 1)
1309 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1310 if (item >= itemidx)
1311 return itempos * me.itemHeight;
1313 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1314 return item * me.itemHeight;
1316 float XonoticServerList_getItemHeight(entity me, int 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;