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, float, vector, float))
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)
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);
1023 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1024 m = tokenizebyseparator(s, ":");
1029 versionstr = argv(1);
1033 for(j = 2; j < m; ++j)
1037 k = substring(argv(j), 0, 1);
1038 v = substring(argv(j), 1, -1);
1042 freeslots = stof(v);
1049 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1051 modname = "Xonotic";
1055 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1056 s = gethostcachestring(SLIST_FIELD_MOD, i);
1058 if(modname == "Xonotic")
1062 // list the mods here on which the pure server check actually works
1063 if(modname != "Xonotic")
1064 if(modname != "InstaGib" || modname != "MinstaGib")
1065 if(modname != "CTS")
1066 if(modname != "NIX")
1067 if(modname != "NewToys")
1070 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1071 theAlpha = SKINALPHA_SERVERLIST_FULL;
1072 else if(freeslots == 0)
1073 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1074 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1075 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1079 p = gethostcachenumber(SLIST_FIELD_PING, i);
1080 const int PING_LOW = 75;
1081 const int PING_MED = 200;
1082 const int PING_HIGH = 500;
1084 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1085 else if(p < PING_MED)
1086 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1087 else if(p < PING_HIGH)
1089 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1090 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1095 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1098 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1100 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1101 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1104 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1106 isv4 = isv6 = false;
1107 if(substring(s, 0, 1) == "[")
1112 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1118 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1119 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1121 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1122 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1127 if(cvar("crypto_aeslevel") >= 2)
1132 if(cvar("crypto_aeslevel") >= 1)
1142 // 2: AES recommended but not available
1143 // 3: AES possible and will be used
1144 // 4: AES recommended and will be used
1150 vector iconSize = '0 0 0';
1151 iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1152 iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1154 vector iconPos = '0 0 0';
1155 iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1156 iconPos_y = (1 - iconSize.y) * 0.5;
1160 if (!(me.seenIPv4 && me.seenIPv6))
1162 iconPos.x += iconSize.x * 0.5;
1164 else if(me.seenIPv4 && me.seenIPv6)
1168 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1170 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1172 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1173 iconPos.x += iconSize.x;
1178 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1179 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1181 iconPos.x += iconSize.x;
1183 if(modname == "Xonotic")
1187 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1188 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1193 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1194 if(draw_PictureSize(n) == '0 0 0')
1195 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1197 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1199 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1201 iconPos.x += iconSize.x;
1203 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1205 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1206 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1208 iconPos.x += iconSize.x;
1216 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1219 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1220 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1223 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1224 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1227 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1228 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1230 // server playercount
1231 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1232 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1235 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1239 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1240 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1242 if(scan == K_ENTER || scan == K_KP_ENTER)
1244 ServerList_Connect_Click(NULL, me);
1247 else if(scan == K_MOUSE2 || scan == K_SPACE)
1251 m_play_click_sound(MENU_SOUND_OPEN);
1252 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1253 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1258 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1262 me.toggleFavorite(me, me.selectedServer);
1263 me.ipAddressBoxFocused = -1;
1268 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1270 else if(!me.controlledTextbox)
1273 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1276 float XonoticServerList_getTotalHeight(entity me)
1278 float num_normal_rows = me.nItems;
1279 int num_headers = category_draw_count;
1280 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1282 int XonoticServerList_getItemAtPos(entity me, float pos)
1284 pos = pos / me.itemHeight;
1286 for (i = category_draw_count - 1; i >= 0; --i) {
1287 int itemidx = category_item[i];
1288 float itempos = i * me.categoriesHeight + category_item[i];
1289 if (pos >= itempos + me.categoriesHeight + 1)
1290 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1294 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1297 float XonoticServerList_getItemStart(entity me, int item)
1300 for (i = category_draw_count - 1; i >= 0; --i) {
1301 int itemidx = category_item[i];
1302 float itempos = i * me.categoriesHeight + category_item[i];
1303 if (item >= itemidx + 1)
1304 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1305 if (item >= itemidx)
1306 return itempos * me.itemHeight;
1308 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1309 return item * me.itemHeight;
1311 float XonoticServerList_getItemHeight(entity me, int item)
1314 for (i = 0; i < category_draw_count; ++i) {
1315 // Matches exactly the headings with increased height.
1316 if (item == category_item[i])
1317 return me.itemHeight * (me.categoriesHeight + 1);
1319 return me.itemHeight;