2 CLASS(XonoticServerList) EXTENDS(XonoticListBox)
3 METHOD(XonoticServerList, configureXonoticServerList, void(entity))
4 ATTRIB(XonoticServerList, rowsPerItem, float, 1)
5 METHOD(XonoticServerList, draw, void(entity))
6 METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
7 METHOD(XonoticServerList, clickListBoxItem, void(entity, float, vector))
8 METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
9 METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
10 METHOD(XonoticServerList, toggleFavorite, void(entity, string))
12 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
14 ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
15 ATTRIB(XonoticServerList, realUpperMargin, float, 0)
16 ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
17 ATTRIB(XonoticServerList, columnIconsSize, float, 0)
18 ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
19 ATTRIB(XonoticServerList, columnPingSize, float, 0)
20 ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
21 ATTRIB(XonoticServerList, columnNameSize, float, 0)
22 ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
23 ATTRIB(XonoticServerList, columnMapSize, float, 0)
24 ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
25 ATTRIB(XonoticServerList, columnTypeSize, float, 0)
26 ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
27 ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
29 ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
30 METHOD(XonoticServerList, setSelected, void(entity, float))
31 METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
32 ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
33 ATTRIB(XonoticServerList, filterShowFull, float, 1)
34 ATTRIB(XonoticServerList, filterString, string, string_null)
35 ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
36 ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
37 ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
38 ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
39 METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
40 ATTRIB(XonoticServerList, needsRefresh, float, 1)
41 METHOD(XonoticServerList, focusEnter, void(entity))
42 METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
43 ATTRIB(XonoticServerList, sortButton1, entity, NULL)
44 ATTRIB(XonoticServerList, sortButton2, entity, NULL)
45 ATTRIB(XonoticServerList, sortButton3, entity, NULL)
46 ATTRIB(XonoticServerList, sortButton4, entity, NULL)
47 ATTRIB(XonoticServerList, sortButton5, entity, NULL)
48 ATTRIB(XonoticServerList, connectButton, entity, NULL)
49 ATTRIB(XonoticServerList, infoButton, entity, NULL)
50 ATTRIB(XonoticServerList, currentSortOrder, float, 0)
51 ATTRIB(XonoticServerList, currentSortField, float, -1)
52 ATTRIB(XonoticServerList, lastBumpSelectTime, float, 0)
53 ATTRIB(XonoticServerList, lastClickedServer, float, -1)
54 ATTRIB(XonoticServerList, lastClickedTime, float, 0)
56 ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
58 ATTRIB(XonoticServerList, seenIPv4, float, 0)
59 ATTRIB(XonoticServerList, seenIPv6, float, 0)
60 ATTRIB(XonoticServerList, categoriesHeight, float, 1.5)
62 METHOD(XonoticServerList, getTotalHeight, float(entity))
63 METHOD(XonoticServerList, getItemAtPos, float(entity, float))
64 METHOD(XonoticServerList, getItemStart, float(entity, float))
65 METHOD(XonoticServerList, getItemHeight, float(entity, float))
66 ENDCLASS(XonoticServerList)
67 entity makeXonoticServerList();
69 #ifndef IMPLEMENTATION
70 var float autocvar_menu_slist_categories = TRUE;
71 var float autocvar_menu_slist_categories_onlyifmultiple = TRUE;
72 var float autocvar_menu_slist_purethreshold = 10;
73 var float autocvar_menu_slist_modimpurity = 10;
74 var float autocvar_menu_slist_recommendations = 2;
75 var float autocvar_menu_slist_recommendations_maxping = 150;
76 var float autocvar_menu_slist_recommendations_minfreeslots = 1;
77 var float autocvar_menu_slist_recommendations_minhumans = 1;
78 var float autocvar_menu_slist_recommendations_purethreshold = -1;
79 //var string autocvar_menu_slist_recommended = "76.124.107.5:26004";
81 // server cache fields
82 #define SLIST_FIELDS \
83 SLIST_FIELD(CNAME, "cname") \
84 SLIST_FIELD(PING, "ping") \
85 SLIST_FIELD(GAME, "game") \
86 SLIST_FIELD(MOD, "mod") \
87 SLIST_FIELD(MAP, "map") \
88 SLIST_FIELD(NAME, "name") \
89 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
90 SLIST_FIELD(NUMPLAYERS, "numplayers") \
91 SLIST_FIELD(NUMHUMANS, "numhumans") \
92 SLIST_FIELD(NUMBOTS, "numbots") \
93 SLIST_FIELD(PROTOCOL, "protocol") \
94 SLIST_FIELD(FREESLOTS, "freeslots") \
95 SLIST_FIELD(PLAYERS, "players") \
96 SLIST_FIELD(QCSTATUS, "qcstatus") \
97 SLIST_FIELD(CATEGORY, "category") \
98 SLIST_FIELD(ISFAVORITE, "isfavorite")
100 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
104 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
105 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
106 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
107 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
109 // function declarations
110 entity RetrieveCategoryEnt(float catnum);
112 float IsServerInList(string list, string srv);
113 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
114 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
116 float CheckCategoryOverride(float cat);
117 float CheckCategoryForEntry(float entry);
118 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
120 void RegisterSLCategories();
122 void ServerList_Connect_Click(entity btn, entity me);
123 void ServerList_Categories_Click(entity box, entity me);
124 void ServerList_ShowEmpty_Click(entity box, entity me);
125 void ServerList_ShowFull_Click(entity box, entity me);
126 void ServerList_Filter_Change(entity box, entity me);
127 void ServerList_Favorite_Click(entity btn, entity me);
128 void ServerList_Info_Click(entity btn, entity me);
129 void ServerList_Update_favoriteButton(entity btn, entity me);
131 // fields for category entities
132 #define MAX_CATEGORIES 9
133 #define CATEGORY_FIRST 1
134 entity categories[MAX_CATEGORIES];
135 float category_ent_count;
138 .string cat_enoverride_string;
139 .string cat_dioverride_string;
140 .float cat_enoverride;
141 .float cat_dioverride;
143 // fields for drawing categories
144 float category_name[MAX_CATEGORIES];
145 float category_item[MAX_CATEGORIES];
146 float category_draw_count;
148 #define SLIST_CATEGORIES \
149 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
150 SLIST_CATEGORY(CAT_RECOMMENDED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Recommended"))) \
151 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
152 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
153 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
154 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
155 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
156 SLIST_CATEGORY(CAT_MINSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^MinstaGib Mode"))) \
157 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
159 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
160 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
162 var string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
164 #undef SLIST_CATEGORY
168 #ifdef IMPLEMENTATION
170 void RegisterSLCategories()
173 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
174 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
175 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
177 categories[name - 1] = cat; \
178 cat.classname = "slist_category"; \
179 cat.cat_name = strzone(#name); \
180 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
181 cat.cat_dioverride_string = strzone(dioverride); \
182 cat.cat_string = strzone(str);
184 #undef SLIST_CATEGORY
189 #define PROCESS_OVERRIDE(override_string,override_field) \
190 for(i = 0; i < category_ent_count; ++i) \
192 s = categories[i].override_string; \
193 if((s != "") && (s != categories[i].cat_name)) \
196 for(x = 0; x < category_ent_count; ++x) \
197 { if(categories[x].cat_name == s) { \
203 strunzone(categories[i].override_string); \
204 categories[i].override_field = catnum; \
210 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
212 categories[i].cat_name \
216 strunzone(categories[i].override_string); \
217 categories[i].override_field = 0; \
219 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
220 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
221 #undef PROCESS_OVERRIDE
224 // Supporting Functions
225 entity RetrieveCategoryEnt(float catnum)
227 if((catnum > 0) && (catnum <= category_ent_count))
229 return categories[catnum - 1];
233 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
238 float IsServerInList(string list, string srv)
244 srv = netaddress_resolve(srv, 26000);
247 p = crypto_getidfp(srv);
248 n = tokenize_console(list);
249 for(i = 0; i < n; ++i)
251 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
259 if(srv == netaddress_resolve(argv(i), 26000))
266 float CheckCategoryOverride(float cat)
268 entity catent = RetrieveCategoryEnt(cat);
271 float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
272 if(override) { return override; }
277 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
282 float CheckCategoryForEntry(float entry)
284 string s, k, v, modtype = "";
285 float j, m, impure = 0, freeslots = 0, sflags = 0;
286 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
287 m = tokenizebyseparator(s, ":");
289 for(j = 2; j < m; ++j)
291 if(argv(j) == "") { break; }
292 k = substring(argv(j), 0, 1);
293 v = substring(argv(j), 1, -1);
296 case "P": { impure = stof(v); break; }
297 case "S": { freeslots = stof(v); break; }
298 case "F": { sflags = stof(v); break; }
299 case "M": { modtype = strtolower(v); break; }
303 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
305 // check if this server is favorited
306 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
308 // now check if it's recommended
309 if(autocvar_menu_slist_recommendations)
311 float recommended = 0;
312 if(autocvar_menu_slist_recommendations & 1)
314 if(IsRecommended(gethostcachestring(SLIST_FIELD_CNAME, entry)))
319 if(autocvar_menu_slist_recommendations & 2)
322 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
325 (autocvar_menu_slist_recommendations_purethreshold < 0)
327 (impure <= autocvar_menu_slist_recommendations_purethreshold)
331 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
333 autocvar_menu_slist_recommendations_minhumans
337 gethostcachenumber(SLIST_FIELD_PING, entry)
339 autocvar_menu_slist_recommendations_maxping
346 if(recommended > 0) { return CAT_RECOMMENDED; }
349 // if not favorited or recommended, check modname
350 if(modtype != "xonotic")
354 // old servers which don't report their mod name are considered modified now
355 case "": { return CAT_MODIFIED; }
357 case "xpm": { return CAT_XPM; }
358 case "minstagib": { return CAT_MINSTAGIB; }
359 case "overkill": { return CAT_OVERKILL; }
360 //case "nix": { return CAT_NIX; }
361 //case "newtoys": { return CAT_NEWTOYS; }
363 // "cts" is allowed as compat, xdf is replacement
365 case "xdf": { return CAT_DEFRAG; }
367 default: { dprint(sprintf("Found strange mod type: %s\n", modtype)); return CAT_MODIFIED; }
371 // must be normal or impure server
372 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
375 float CheckItemNumber(float num)
379 if not(category_draw_count) { return num; } // there are no categories to process
381 for(i = 0, n = 1; n <= category_draw_count; ++i, ++n)
383 if(category_item[i] == (num - i)) { return -category_name[i]; }
384 else if(n == category_draw_count) { return (num - n); }
385 else if((num - i) <= category_item[n]) { return (num - n); }
388 // should never hit this point
389 error(sprintf("CheckItemNumber(%d): Function fell through without normal return!\n", num));
393 void XonoticServerList_toggleFavorite(entity me, string srv)
395 string s, s0, s1, s2, srv_resolved, p;
397 srv_resolved = netaddress_resolve(srv, 26000);
398 p = crypto_getidfp(srv_resolved);
399 s = cvar_string("net_slist_favorites");
400 n = tokenize_console(s);
402 for(i = 0; i < n; ++i)
404 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
412 if(srv_resolved != netaddress_resolve(argv(i), 26000))
417 s0 = substring(s, 0, argv_end_index(i - 1));
419 s2 = substring(s, argv_start_index(i + 1), -1);
420 if(s0 != "" && s2 != "")
422 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
423 s = cvar_string("net_slist_favorites");
424 n = tokenize_console(s);
435 cvar_set("net_slist_favorites", strcat(s, s1, p));
437 cvar_set("net_slist_favorites", strcat(s, s1, srv));
440 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
443 void ServerList_Update_favoriteButton(entity btn, entity me)
445 me.favoriteButton.setText(me.favoriteButton,
446 (IsFavorite(me.ipAddressBox.text) ?
447 _("Remove") : _("Bookmark")
452 entity makeXonoticServerList()
455 me = spawnXonoticServerList();
456 me.configureXonoticServerList(me);
459 void XonoticServerList_configureXonoticServerList(entity me)
461 me.configureXonoticListBox(me);
464 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
471 void XonoticServerList_setSelected(entity me, float i)
473 // todo: add logic to skip categories
475 save = me.selectedItem;
476 SUPER(XonoticServerList).setSelected(me, i);
478 if(me.selectedItem == save)
484 //if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != CheckItemNumber(me.nItems))
485 // { error("^1XonoticServerList_setSelected(); ERROR: ^7Host cache viewcount mismatches nItems!\n"); return; } // sorry, it would be wrong
486 // ^ todo: make this work somehow?
488 #define SET_SELECTED_SERVER(cachenum) \
489 if(me.selectedServer) { strunzone(me.selectedServer); } \
490 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, cachenum)); \
491 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer); \
492 me.ipAddressBox.cursorPos = strlen(me.selectedServer); \
493 me.ipAddressBoxFocused = -1; \
496 num = CheckItemNumber(me.selectedItem);
498 if(num >= 0) { SET_SELECTED_SERVER(num); }
499 else if(save > me.selectedItem)
501 if(me.selectedItem == 0) { return; }
504 if(me.lastClickedTime >= me.lastBumpSelectTime)
506 SUPER(XonoticServerList).setSelected(me, me.selectedItem - 1);
507 num = CheckItemNumber(me.selectedItem);
510 me.lastBumpSelectTime = time;
511 SET_SELECTED_SERVER(num);
516 else if(save < me.selectedItem)
518 if(me.selectedItem == me.nItems) { return; }
521 if(me.lastClickedTime >= me.lastBumpSelectTime)
523 SUPER(XonoticServerList).setSelected(me, me.selectedItem + 1);
524 num = CheckItemNumber(me.selectedItem);
527 me.lastBumpSelectTime = time;
528 SET_SELECTED_SERVER(num);
535 void XonoticServerList_refreshServerList(entity me, float mode)
537 //print("refresh of type ", ftos(mode), "\n");
539 if(mode >= REFRESHSERVERLIST_REFILTER)
543 string s, typestr, modstr;
547 m = strstrofs(s, ":", 0);
550 typestr = substring(s, 0, m);
551 s = substring(s, m + 1, strlen(s) - m - 1);
552 while(substring(s, 0, 1) == " ")
553 s = substring(s, 1, strlen(s) - 1);
558 modstr = cvar_string("menu_slist_modfilter");
560 m = SLIST_MASK_AND - 1;
561 resethostcachemasks();
563 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
564 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
567 if(!me.filterShowFull)
569 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
570 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
574 if(!me.filterShowEmpty)
575 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
577 // gametype filtering
579 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
584 if(substring(modstr, 0, 1) == "!")
585 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
587 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
591 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
592 for(i = 0; i < n; ++i)
594 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
596 m = SLIST_MASK_OR - 1;
599 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
600 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
601 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
602 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
606 //listflags |= SLSF_FAVORITES;
607 listflags |= SLSF_CATEGORIES;
608 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
609 sethostcachesort(me.currentSortField, listflags);
613 if(mode >= REFRESHSERVERLIST_ASK)
614 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
616 void XonoticServerList_focusEnter(entity me)
618 if(time < me.nextRefreshTime)
620 //print("sorry, no refresh yet\n");
623 me.nextRefreshTime = time + 10;
624 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
627 void XonoticServerList_draw(entity me)
629 float i, found, owned, num;
631 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
635 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
638 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
642 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
645 if(me.currentSortField == -1)
647 me.setSortOrder(me, SLIST_FIELD_PING, +1);
648 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
650 else if(me.needsRefresh == 1)
652 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
654 else if(me.needsRefresh == 2)
657 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
659 else if(me.needsRefresh == 3)
662 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
665 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
667 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
668 category_draw_count = 0;
670 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
672 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
673 me.nItems = itemcount;
675 //float visible = floor(me.scrollPos / me.itemHeight);
676 // ^ unfortunately no such optimization can be made-- we must process through the
677 // entire list, otherwise there is no way to know which item is first in its category.
679 // binary search method suggested by div
682 for(x = 1; x <= category_ent_count; ++x) {
684 float last = (itemcount - 1);
689 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
690 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
692 // The first one is already > x.
693 // Therefore, category x does not exist.
694 // Higher numbered categories do exist though.
695 } else if (catl < x) {
696 // The last one is < x.
697 // Thus this category - and any following -
700 } else if (catf == x) {
701 // Starts at first. This breaks the loop
702 // invariant in the binary search and thus has
703 // to be handled separately.
704 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
705 error("Category mismatch I");
707 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
708 error("Category mismatch II");
709 category_name[category_draw_count] = x;
710 category_item[category_draw_count] = first;
711 ++category_draw_count;
715 // At this point, catf <= x < catl, thus
716 // catf < catl, thus first < last.
719 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
720 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
723 while (last - first > 1) {
724 float middle = floor((first + last) / 2);
725 // By loop condition, middle != first && middle != last.
726 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
736 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
737 error("Category mismatch III");
739 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
740 error("Category mismatch IV");
741 category_name[category_draw_count] = x;
742 category_item[category_draw_count] = last;
743 ++category_draw_count;
745 begin = last + 1; // already scanned through these, skip 'em
748 begin = last; // already scanned through these, skip 'em
752 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
754 category_name[0] = -1;
755 category_item[0] = -1;
756 category_draw_count = 0;
757 me.nItems = itemcount;
760 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
762 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
763 me.infoButton.disabled = ((me.nItems == 0) || !owned);
764 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
767 if(me.selectedServer)
769 for(i = 0; i < me.nItems; ++i)
771 num = CheckItemNumber(i);
774 if(gethostcachestring(SLIST_FIELD_CNAME, num) == me.selectedServer)
776 if(i != me.selectedItem)
778 me.lastClickedServer = -1;
791 if(me.selectedItem >= me.nItems) { me.selectedItem = me.nItems - 1; }
792 if(me.selectedServer) { strunzone(me.selectedServer); }
794 num = CheckItemNumber(me.selectedItem);
795 if(num >= 0) { me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, num)); }
801 if(me.selectedServer != me.ipAddressBox.text)
803 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
804 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
805 me.ipAddressBoxFocused = -1;
809 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
811 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
812 ServerList_Update_favoriteButton(NULL, me);
813 me.ipAddressBoxFocused = me.ipAddressBox.focused;
816 SUPER(XonoticServerList).draw(me);
818 void ServerList_PingSort_Click(entity btn, entity me)
820 me.setSortOrder(me, SLIST_FIELD_PING, +1);
822 void ServerList_NameSort_Click(entity btn, entity me)
824 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
826 void ServerList_MapSort_Click(entity btn, entity me)
828 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
830 void ServerList_PlayerSort_Click(entity btn, entity me)
832 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
834 void ServerList_TypeSort_Click(entity btn, entity me)
839 m = strstrofs(s, ":", 0);
842 s = substring(s, 0, m);
843 while(substring(s, m+1, 1) == " ") // skip spaces
849 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
851 t = MapInfo_Type_ToString(i);
853 if(t == "") // it repeats (default case)
856 // choose the first one
857 s = MapInfo_Type_ToString(1);
862 // the type was found
863 // choose the next one
864 s = MapInfo_Type_ToString(i * 2);
866 s = MapInfo_Type_ToString(1);
873 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
875 me.controlledTextbox.setText(me.controlledTextbox, s);
876 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
877 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
878 //ServerList_Filter_Change(me.controlledTextbox, me);
880 void ServerList_Filter_Change(entity box, entity me)
883 strunzone(me.filterString);
885 me.filterString = strzone(box.text);
887 me.filterString = string_null;
888 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
890 me.ipAddressBox.setText(me.ipAddressBox, "");
891 me.ipAddressBox.cursorPos = 0;
892 me.ipAddressBoxFocused = -1;
894 void ServerList_Categories_Click(entity box, entity me)
896 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
897 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
899 me.ipAddressBox.setText(me.ipAddressBox, "");
900 me.ipAddressBox.cursorPos = 0;
901 me.ipAddressBoxFocused = -1;
903 void ServerList_ShowEmpty_Click(entity box, entity me)
905 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
906 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
908 me.ipAddressBox.setText(me.ipAddressBox, "");
909 me.ipAddressBox.cursorPos = 0;
910 me.ipAddressBoxFocused = -1;
912 void ServerList_ShowFull_Click(entity box, entity me)
914 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
915 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
917 me.ipAddressBox.setText(me.ipAddressBox, "");
918 me.ipAddressBox.cursorPos = 0;
919 me.ipAddressBoxFocused = -1;
921 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
923 if(me.currentSortField == fld)
924 direction = -me.currentSortOrder;
925 me.currentSortOrder = direction;
926 me.currentSortField = fld;
927 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
928 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
929 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
930 me.sortButton4.forcePressed = 0;
931 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
933 if(me.selectedServer)
934 strunzone(me.selectedServer);
935 me.selectedServer = string_null;
936 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
938 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
940 vector originInLBSpace, sizeInLBSpace;
941 originInLBSpace = eY * (-me.itemHeight);
942 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
944 vector originInDialogSpace, sizeInDialogSpace;
945 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
946 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
948 btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
949 btn.Container_size_x = sizeInDialogSpace_x * theSize;
950 btn.setText(btn, theTitle);
951 btn.onClick = theFunc;
952 btn.onClickEntity = me;
955 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
957 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
959 me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
960 me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
961 me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
963 me.columnIconsOrigin = 0;
964 me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
965 me.columnPingSize = me.realFontSize_x * 3;
966 me.columnMapSize = me.realFontSize_x * 10;
967 me.columnTypeSize = me.realFontSize_x * 4;
968 me.columnPlayersSize = me.realFontSize_x * 5;
969 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
970 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
971 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
972 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
973 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
974 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
976 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
977 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
978 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
979 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
980 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
983 f = me.currentSortField;
986 me.currentSortField = -1;
987 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
990 void ServerList_Connect_Click(entity btn, entity me)
992 localcmd(sprintf("connect %s\n",
993 ((me.ipAddressBox.text != "") ?
994 me.ipAddressBox.text : me.selectedServer
998 void ServerList_Favorite_Click(entity btn, entity me)
1001 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
1004 me.toggleFavorite(me, me.ipAddressBox.text);
1005 me.ipAddressBoxFocused = -1;
1008 void ServerList_Info_Click(entity btn, entity me)
1010 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, CheckItemNumber(me.selectedItem));
1011 DialogOpenButton_Click(me, main.serverInfoDialog);
1013 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
1015 float num = CheckItemNumber(i);
1018 if(num == me.lastClickedServer)
1019 if(time < me.lastClickedTime + 0.3)
1022 ServerList_Connect_Click(NULL, me);
1024 me.lastClickedServer = num;
1025 me.lastClickedTime = time;
1028 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
1030 // layout: Ping, Server name, Map name, NP, TP, MP
1035 float m, pure, freeslots, j, sflags;
1036 string s, typestr, versionstr, k, v, modname;
1038 float item = CheckItemNumber(i);
1039 //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
1041 vector oldscale = draw_scale;
1042 vector oldshift = draw_shift;
1043 #define SET_YRANGE(start,end) \
1044 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
1045 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
1049 entity catent = RetrieveCategoryEnt(-item);
1052 float delta = (1 - 1 / me.categoriesHeight);
1053 SET_YRANGE(delta, 1); // bottom align
1055 eY * me.realUpperMargin
1057 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1070 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1072 s = gethostcachestring(SLIST_FIELD_QCSTATUS, item);
1073 m = tokenizebyseparator(s, ":");
1078 versionstr = argv(1);
1084 for(j = 2; j < m; ++j)
1088 k = substring(argv(j), 0, 1);
1089 v = substring(argv(j), 1, -1);
1093 freeslots = stof(v);
1100 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1102 modname = "Xonotic";
1106 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1107 s = gethostcachestring(SLIST_FIELD_MOD, item);
1109 if(modname == "Xonotic")
1113 // list the mods here on which the pure server check actually works
1114 if(modname != "Xonotic")
1115 if(modname != "MinstaGib")
1116 if(modname != "CTS")
1117 if(modname != "NIX")
1118 if(modname != "NewToys")
1121 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, item) <= 0)
1122 theAlpha = SKINALPHA_SERVERLIST_FULL;
1123 else if(freeslots == 0)
1124 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1125 else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item))
1126 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1130 p = gethostcachenumber(SLIST_FIELD_PING, item);
1132 #define PING_MED 200
1133 #define PING_HIGH 500
1135 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1136 else if(p < PING_MED)
1137 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1138 else if(p < PING_HIGH)
1140 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1141 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1146 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1149 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, item))
1151 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1152 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1155 s = gethostcachestring(SLIST_FIELD_CNAME, item);
1158 if(substring(s, 0, 1) == "[")
1163 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1169 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1170 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1172 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1173 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1178 if(cvar("crypto_aeslevel") >= 2)
1183 if(cvar("crypto_aeslevel") >= 1)
1193 // 2: AES recommended but not available
1194 // 3: AES possible and will be used
1195 // 4: AES recommended and will be used
1201 vector iconSize = '0 0 0';
1202 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1203 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1205 vector iconPos = '0 0 0';
1206 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1207 iconPos_y = (1 - iconSize_y) * 0.5;
1211 if not(me.seenIPv4 && me.seenIPv6)
1213 iconPos_x += iconSize_x * 0.5;
1215 else if(me.seenIPv4 && me.seenIPv6)
1219 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1221 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1223 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1224 iconPos_x += iconSize_x;
1229 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1230 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1232 iconPos_x += iconSize_x;
1234 if(modname == "Xonotic")
1238 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1239 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1244 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1245 if(draw_PictureSize(n) == '0 0 0')
1246 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1248 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1250 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1252 iconPos_x += iconSize_x;
1254 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1256 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1257 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1259 iconPos_x += iconSize_x;
1267 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1270 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, item), me.columnNameSize, 0, me.realFontSize);
1271 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1274 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, item), me.columnMapSize, 0, me.realFontSize);
1275 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1278 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1279 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1281 // server playercount
1282 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, item)));
1283 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1286 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1288 float i = CheckItemNumber(me.selectedItem);
1291 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1292 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1294 me.lastBumpSelectTime = 0;
1296 if(scan == K_ENTER || scan == K_KP_ENTER)
1298 ServerList_Connect_Click(NULL, me);
1301 else if(scan == K_MOUSE2 || scan == K_SPACE)
1303 if((me.nItems != 0) && (i >= 0))
1305 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
1306 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1311 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1313 if((me.nItems != 0) && (i >= 0))
1315 me.toggleFavorite(me, me.selectedServer);
1316 me.ipAddressBoxFocused = -1;
1321 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1323 else if(!me.controlledTextbox)
1326 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1329 float XonoticServerList_getTotalHeight(entity me) {
1330 float num_normal_rows = me.nItems - category_draw_count;
1331 float num_headers = category_draw_count;
1332 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1334 float XonoticServerList_getItemAtPos(entity me, float pos) {
1335 pos = pos / me.itemHeight;
1336 // TODO when item+category merging is done, manually expand these macros
1337 #define ITEM_STARTPOS_SINGLE(itemidx, itempos) \
1338 if (pos >= itempos) \
1340 #define ITEM_STARTPOS_MULTI(itemidx, itempos) \
1341 if (pos >= itempos) \
1342 return itemidx + floor(pos - (itempos))
1344 for (i = category_draw_count - 1; i >= 0; --i) {
1345 ITEM_STARTPOS_MULTI((i + 1) + category_item[i], (i + 1) * me.categoriesHeight + category_item[i]);
1346 ITEM_STARTPOS_SINGLE(i + category_item[i], i * me.categoriesHeight + category_item[i]);
1348 #undef ITEM_STARTPOS_MULTI
1349 #undef ITEM_STARTPOS_SINGLE
1350 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1353 float XonoticServerList_getItemStart(entity me, float item) {
1354 // TODO when item+category merging is done, manually expand these macros
1355 #define ITEM_STARTPOS_SINGLE(itemidx, itempos) \
1356 if (item >= itemidx) \
1357 return (itempos) * me.itemHeight
1358 #define ITEM_STARTPOS_MULTI(itemidx, itempos) \
1359 if (item >= itemidx) \
1360 return ((itempos) + (item - (itemidx))) * me.itemHeight
1362 for (i = category_draw_count - 1; i >= 0; --i) {
1363 ITEM_STARTPOS_MULTI((i + 1) + category_item[i], (i + 1) * me.categoriesHeight + category_item[i]);
1364 ITEM_STARTPOS_SINGLE(i + category_item[i], i * me.categoriesHeight + category_item[i]);
1366 #undef ITEM_STARTPOS_MULTI
1367 #undef ITEM_STARTPOS_SINGLE
1368 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1369 return item * me.itemHeight;
1371 float XonoticServerList_getItemHeight(entity me, float item) {
1373 for (i = 0; i < category_draw_count; ++i) {
1374 // Matches exactly the headings with increased height.
1375 float first = i + category_item[i];
1377 return me.itemHeight * me.categoriesHeight;
1379 return me.itemHeight;