3 CLASS(XonoticServerList, XonoticListBox)
4 METHOD(XonoticServerList, configureXonoticServerList, void(entity))
5 ATTRIB(XonoticServerList, rowsPerItem, float, 1)
6 METHOD(XonoticServerList, draw, void(entity))
7 METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
8 METHOD(XonoticServerList, doubleClickListBoxItem, void(entity, float, vector))
9 METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
10 METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
11 METHOD(XonoticServerList, toggleFavorite, void(entity, string))
13 ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
15 ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
16 ATTRIB(XonoticServerList, realUpperMargin, float, 0)
17 ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
18 ATTRIB(XonoticServerList, columnIconsSize, float, 0)
19 ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
20 ATTRIB(XonoticServerList, columnPingSize, float, 0)
21 ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
22 ATTRIB(XonoticServerList, columnNameSize, float, 0)
23 ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
24 ATTRIB(XonoticServerList, columnMapSize, float, 0)
25 ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
26 ATTRIB(XonoticServerList, columnTypeSize, float, 0)
27 ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
28 ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
30 ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
31 METHOD(XonoticServerList, setSelected, void(entity, float))
32 METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
33 ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
34 ATTRIB(XonoticServerList, filterShowFull, float, 1)
35 ATTRIB(XonoticServerList, filterString, string, string_null)
36 ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
37 ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
38 ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
39 ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
40 METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
41 ATTRIB(XonoticServerList, needsRefresh, float, 1)
42 METHOD(XonoticServerList, focusEnter, void(entity))
43 METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
44 ATTRIB(XonoticServerList, sortButton1, entity, NULL)
45 ATTRIB(XonoticServerList, sortButton2, entity, NULL)
46 ATTRIB(XonoticServerList, sortButton3, entity, NULL)
47 ATTRIB(XonoticServerList, sortButton4, entity, NULL)
48 ATTRIB(XonoticServerList, sortButton5, entity, NULL)
49 ATTRIB(XonoticServerList, connectButton, entity, NULL)
50 ATTRIB(XonoticServerList, infoButton, entity, NULL)
51 ATTRIB(XonoticServerList, currentSortOrder, float, 0)
52 ATTRIB(XonoticServerList, currentSortField, float, -1)
54 ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
56 ATTRIB(XonoticServerList, seenIPv4, float, 0)
57 ATTRIB(XonoticServerList, seenIPv6, float, 0)
58 ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
60 METHOD(XonoticServerList, getTotalHeight, float(entity))
61 METHOD(XonoticServerList, getItemAtPos, float(entity, float))
62 METHOD(XonoticServerList, getItemStart, float(entity, float))
63 METHOD(XonoticServerList, getItemHeight, float(entity, float))
64 ENDCLASS(XonoticServerList)
65 entity makeXonoticServerList();
67 #ifndef IMPLEMENTATION
68 float autocvar_menu_slist_categories;
69 float autocvar_menu_slist_categories_onlyifmultiple;
70 float autocvar_menu_slist_purethreshold;
71 float autocvar_menu_slist_modimpurity;
72 float autocvar_menu_slist_recommendations;
73 float autocvar_menu_slist_recommendations_maxping;
74 float autocvar_menu_slist_recommendations_minfreeslots;
75 float autocvar_menu_slist_recommendations_minhumans;
76 float autocvar_menu_slist_recommendations_purethreshold;
78 // server cache fields
79 #define SLIST_FIELDS \
80 SLIST_FIELD(CNAME, "cname") \
81 SLIST_FIELD(PING, "ping") \
82 SLIST_FIELD(GAME, "game") \
83 SLIST_FIELD(MOD, "mod") \
84 SLIST_FIELD(MAP, "map") \
85 SLIST_FIELD(NAME, "name") \
86 SLIST_FIELD(MAXPLAYERS, "maxplayers") \
87 SLIST_FIELD(NUMPLAYERS, "numplayers") \
88 SLIST_FIELD(NUMHUMANS, "numhumans") \
89 SLIST_FIELD(NUMBOTS, "numbots") \
90 SLIST_FIELD(PROTOCOL, "protocol") \
91 SLIST_FIELD(FREESLOTS, "freeslots") \
92 SLIST_FIELD(PLAYERS, "players") \
93 SLIST_FIELD(QCSTATUS, "qcstatus") \
94 SLIST_FIELD(CATEGORY, "category") \
95 SLIST_FIELD(ISFAVORITE, "isfavorite")
97 #define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
101 const float REFRESHSERVERLIST_RESORT = 0; // sort the server list again to update for changes to e.g. favorite status, categories
102 const float REFRESHSERVERLIST_REFILTER = 1; // ..., also update filter and sort criteria
103 const float REFRESHSERVERLIST_ASK = 2; // ..., also suggest querying servers now
104 const float REFRESHSERVERLIST_RESET = 3; // ..., also clear the list first
106 // function declarations
107 float IsServerInList(string list, string srv);
108 #define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
109 #define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
110 #define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
112 entity RetrieveCategoryEnt(float catnum);
114 float CheckCategoryOverride(float cat);
115 float CheckCategoryForEntry(float entry);
116 float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
118 void RegisterSLCategories();
120 void ServerList_Connect_Click(entity btn, entity me);
121 void ServerList_Categories_Click(entity box, entity me);
122 void ServerList_ShowEmpty_Click(entity box, entity me);
123 void ServerList_ShowFull_Click(entity box, entity me);
124 void ServerList_Filter_Change(entity box, entity me);
125 void ServerList_Favorite_Click(entity btn, entity me);
126 void ServerList_Info_Click(entity btn, entity me);
127 void ServerList_Update_favoriteButton(entity btn, entity me);
129 // fields for category entities
130 const int MAX_CATEGORIES = 9;
131 const int CATEGORY_FIRST = 1;
132 entity categories[MAX_CATEGORIES];
133 int category_ent_count;
136 .string cat_enoverride_string;
137 .string cat_dioverride_string;
138 .float cat_enoverride;
139 .float cat_dioverride;
141 // fields for drawing categories
142 int category_name[MAX_CATEGORIES];
143 int category_item[MAX_CATEGORIES];
144 int category_draw_count;
146 #define SLIST_CATEGORIES \
147 SLIST_CATEGORY(CAT_FAVORITED, "", "", ZCTX(_("SLCAT^Favorites"))) \
148 SLIST_CATEGORY(CAT_RECOMMENDED, "", "", ZCTX(_("SLCAT^Recommended"))) \
149 SLIST_CATEGORY(CAT_NORMAL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Normal Servers"))) \
150 SLIST_CATEGORY(CAT_SERVERS, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Servers"))) \
151 SLIST_CATEGORY(CAT_XPM, "CAT_NORMAL", "CAT_SERVERS", ZCTX(_("SLCAT^Competitive Mode"))) \
152 SLIST_CATEGORY(CAT_MODIFIED, "", "CAT_SERVERS", ZCTX(_("SLCAT^Modified Servers"))) \
153 SLIST_CATEGORY(CAT_OVERKILL, "", "CAT_SERVERS", ZCTX(_("SLCAT^Overkill Mode"))) \
154 SLIST_CATEGORY(CAT_INSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^InstaGib Mode"))) \
155 SLIST_CATEGORY(CAT_DEFRAG, "", "CAT_SERVERS", ZCTX(_("SLCAT^Defrag Mode")))
157 #define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
158 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
160 string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
162 #undef SLIST_CATEGORY
166 #ifdef IMPLEMENTATION
168 void RegisterSLCategories()
171 #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
172 SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
173 CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
175 categories[name - 1] = cat; \
176 cat.classname = "slist_category"; \
177 cat.cat_name = strzone(#name); \
178 cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
179 cat.cat_dioverride_string = strzone(dioverride); \
180 cat.cat_string = strzone(str);
182 #undef SLIST_CATEGORY
187 #define PROCESS_OVERRIDE(override_string,override_field) \
188 for(i = 0; i < category_ent_count; ++i) \
190 s = categories[i].override_string; \
191 if((s != "") && (s != categories[i].cat_name)) \
194 for(x = 0; x < category_ent_count; ++x) \
195 { if(categories[x].cat_name == s) { \
201 strunzone(categories[i].override_string); \
202 categories[i].override_field = catnum; \
208 "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
210 categories[i].cat_name \
214 strunzone(categories[i].override_string); \
215 categories[i].override_field = 0; \
217 PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
218 PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
219 #undef PROCESS_OVERRIDE
222 // Supporting Functions
223 entity RetrieveCategoryEnt(int catnum)
225 if((catnum > 0) && (catnum <= category_ent_count))
227 return categories[catnum - 1];
231 error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
236 bool IsServerInList(string list, string srv)
242 srv = netaddress_resolve(srv, 26000);
245 p = crypto_getidfp(srv);
246 n = tokenize_console(list);
247 for(i = 0; i < n; ++i)
249 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
257 if(srv == netaddress_resolve(argv(i), 26000))
264 int CheckCategoryOverride(int cat)
266 entity catent = RetrieveCategoryEnt(cat);
269 int override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
270 if(override) { return override; }
275 error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
280 int CheckCategoryForEntry(int entry)
282 string s, k, v, modtype = "";
283 int j, m, impure = 0, freeslots = 0, sflags = 0;
284 s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
285 m = tokenizebyseparator(s, ":");
287 for(j = 2; j < m; ++j)
289 if(argv(j) == "") { break; }
290 k = substring(argv(j), 0, 1);
291 v = substring(argv(j), 1, -1);
294 case "P": { impure = stof(v); break; }
295 case "S": { freeslots = stof(v); break; }
296 case "F": { sflags = stof(v); break; }
297 case "M": { modtype = strtolower(v); break; }
301 if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
303 // check if this server is favorited
304 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
306 // now check if it's recommended
307 if(autocvar_menu_slist_recommendations)
309 string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
311 if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
314 float recommended = 0;
315 if(autocvar_menu_slist_recommendations & 1)
317 if(IsRecommended(cname)) { ++recommended; }
318 else { --recommended; }
320 if(autocvar_menu_slist_recommendations & 2)
323 ///// check for minimum free slots
324 (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
326 && // check for purity requirement
328 (autocvar_menu_slist_recommendations_purethreshold < 0)
330 (impure <= autocvar_menu_slist_recommendations_purethreshold)
333 && // check for minimum amount of humans
335 gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
337 autocvar_menu_slist_recommendations_minhumans
340 && // check for maximum latency
342 gethostcachenumber(SLIST_FIELD_PING, entry)
344 autocvar_menu_slist_recommendations_maxping
351 if(recommended > 0) { return CAT_RECOMMENDED; }
355 // if not favorited or recommended, check modname
356 if(modtype != "xonotic")
360 // old servers which don't report their mod name are considered modified now
361 case "": { return CAT_MODIFIED; }
363 case "xpm": { return CAT_XPM; }
365 case "instagib": { return CAT_INSTAGIB; }
366 case "overkill": { return CAT_OVERKILL; }
367 //case "nix": { return CAT_NIX; }
368 //case "newtoys": { return CAT_NEWTOYS; }
370 // "cts" is allowed as compat, xdf is replacement
372 case "xdf": { return CAT_DEFRAG; }
374 default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
378 // must be normal or impure server
379 return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
382 void XonoticServerList_toggleFavorite(entity me, string srv)
384 string s, s0, s1, s2, srv_resolved, p;
387 srv_resolved = netaddress_resolve(srv, 26000);
388 p = crypto_getidfp(srv_resolved);
389 s = cvar_string("net_slist_favorites");
390 n = tokenize_console(s);
391 for(i = 0; i < n; ++i)
393 if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
401 if(srv_resolved != netaddress_resolve(argv(i), 26000))
406 s0 = substring(s, 0, argv_end_index(i - 1));
408 s2 = substring(s, argv_start_index(i + 1), -1);
409 if(s0 != "" && s2 != "")
411 cvar_set("net_slist_favorites", strcat(s0, s1, s2));
412 s = cvar_string("net_slist_favorites");
413 n = tokenize_console(s);
424 cvar_set("net_slist_favorites", strcat(s, s1, p));
426 cvar_set("net_slist_favorites", strcat(s, s1, srv));
429 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
432 void ServerList_Update_favoriteButton(entity btn, entity me)
434 me.favoriteButton.setText(me.favoriteButton,
435 (IsFavorite(me.ipAddressBox.text) ?
436 _("Remove") : _("Favorite")
441 entity makeXonoticServerList()
444 me = NEW(XonoticServerList);
445 me.configureXonoticServerList(me);
448 void XonoticServerList_configureXonoticServerList(entity me)
450 me.configureXonoticListBox(me);
453 #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
460 void XonoticServerList_setSelected(entity me, int i)
462 //int save = me.selectedItem;
463 SUPER(XonoticServerList).setSelected(me, i);
465 if(me.selectedItem == save)
470 if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
471 return; // sorry, it would be wrong
473 if(me.selectedServer)
474 strunzone(me.selectedServer);
475 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
477 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
478 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
479 me.ipAddressBoxFocused = -1;
481 void XonoticServerList_refreshServerList(entity me, int mode)
483 //print("refresh of type ", ftos(mode), "\n");
485 if(mode >= REFRESHSERVERLIST_REFILTER)
490 string s, typestr, modstr;
494 m = strstrofs(s, ":", 0);
497 typestr = substring(s, 0, m);
498 s = substring(s, m + 1, strlen(s) - m - 1);
499 while(substring(s, 0, 1) == " ")
500 s = substring(s, 1, strlen(s) - 1);
505 modstr = cvar_string("menu_slist_modfilter");
507 m = SLIST_MASK_AND - 1;
508 resethostcachemasks();
510 // ping: reject negative ping (no idea why this happens in the first place, engine bug)
511 sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
514 if(!me.filterShowFull)
516 sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
517 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
521 if(!me.filterShowEmpty)
522 sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
524 // gametype filtering
526 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
531 if(substring(modstr, 0, 1) == "!")
532 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
534 sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
538 n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
539 for(i = 0; i < n; ++i)
541 sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
543 m = SLIST_MASK_OR - 1;
546 sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
547 sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
548 sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
549 sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
553 //listflags |= SLSF_FAVORITES;
554 listflags |= SLSF_CATEGORIES;
555 if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
556 sethostcachesort(me.currentSortField, listflags);
560 if(mode >= REFRESHSERVERLIST_ASK)
561 refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
563 void XonoticServerList_focusEnter(entity me)
565 SUPER(XonoticServerList).focusEnter(me);
566 if(time < me.nextRefreshTime)
568 //print("sorry, no refresh yet\n");
571 me.nextRefreshTime = time + 10;
572 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
575 void XonoticServerList_draw(entity me)
578 bool found = false, owned;
580 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
584 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
587 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
591 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
594 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
598 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
601 if(me.currentSortField == -1)
603 me.setSortOrder(me, SLIST_FIELD_PING, +1);
604 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
606 else if(me.needsRefresh == 1)
608 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
610 else if(me.needsRefresh == 2)
613 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
615 else if(me.needsRefresh == 3)
618 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
621 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
623 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
624 category_draw_count = 0;
626 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
628 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
629 me.nItems = itemcount;
631 //float visible = floor(me.scrollPos / me.itemHeight);
632 // ^ unfortunately no such optimization can be made-- we must process through the
633 // entire list, otherwise there is no way to know which item is first in its category.
635 // binary search method suggested by div
638 for(x = 1; x <= category_ent_count; ++x) {
640 float last = (itemcount - 1);
645 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
646 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
648 // The first one is already > x.
649 // Therefore, category x does not exist.
650 // Higher numbered categories do exist though.
651 } else if (catl < x) {
652 // The last one is < x.
653 // Thus this category - and any following -
656 } else if (catf == x) {
657 // Starts at first. This breaks the loop
658 // invariant in the binary search and thus has
659 // to be handled separately.
660 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
661 error("Category mismatch I");
663 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
664 error("Category mismatch II");
665 category_name[category_draw_count] = x;
666 category_item[category_draw_count] = first;
667 ++category_draw_count;
670 // At this point, catf <= x < catl, thus
671 // catf < catl, thus first < last.
674 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
675 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
678 while (last - first > 1) {
679 float middle = floor((first + last) / 2);
680 // By loop condition, middle != first && middle != last.
681 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
691 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
692 error("Category mismatch III");
694 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
695 error("Category mismatch IV");
696 category_name[category_draw_count] = x;
697 category_item[category_draw_count] = last;
698 ++category_draw_count;
699 begin = last + 1; // already scanned through these, skip 'em
702 begin = last; // already scanned through these, skip 'em
705 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
707 category_name[0] = -1;
708 category_item[0] = -1;
709 category_draw_count = 0;
710 me.nItems = itemcount;
713 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
715 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
716 me.infoButton.disabled = ((me.nItems == 0) || !owned);
717 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
719 if(me.selectedServer)
721 for(i = 0; i < me.nItems; ++i)
723 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
735 if(me.selectedItem >= me.nItems)
736 me.selectedItem = me.nItems - 1;
737 if(me.selectedServer)
738 strunzone(me.selectedServer);
739 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
745 if(me.selectedServer != me.ipAddressBox.text)
747 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
748 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
749 me.ipAddressBoxFocused = -1;
753 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
755 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
756 ServerList_Update_favoriteButton(NULL, me);
757 me.ipAddressBoxFocused = me.ipAddressBox.focused;
760 SUPER(XonoticServerList).draw(me);
762 void ServerList_PingSort_Click(entity btn, entity me)
764 me.setSortOrder(me, SLIST_FIELD_PING, +1);
766 void ServerList_NameSort_Click(entity btn, entity me)
768 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
770 void ServerList_MapSort_Click(entity btn, entity me)
772 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
774 void ServerList_PlayerSort_Click(entity btn, entity me)
776 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
778 void ServerList_TypeSort_Click(entity btn, entity me)
783 m = strstrofs(s, ":", 0);
786 s = substring(s, 0, m);
787 while(substring(s, m+1, 1) == " ") // skip spaces
793 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
795 t = MapInfo_Type_ToString(i);
797 if(t == "") // it repeats (default case)
800 // choose the first one
801 s = MapInfo_Type_ToString(1);
806 // the type was found
807 // choose the next one
808 s = MapInfo_Type_ToString(i * 2);
810 s = MapInfo_Type_ToString(1);
817 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
819 me.controlledTextbox.setText(me.controlledTextbox, s);
820 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
821 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
822 //ServerList_Filter_Change(me.controlledTextbox, me);
824 void ServerList_Filter_Change(entity box, entity me)
827 strunzone(me.filterString);
829 me.filterString = strzone(box.text);
831 me.filterString = string_null;
832 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
834 me.ipAddressBox.setText(me.ipAddressBox, "");
835 me.ipAddressBox.cursorPos = 0;
836 me.ipAddressBoxFocused = -1;
838 void ServerList_Categories_Click(entity box, entity me)
840 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
841 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
843 me.ipAddressBox.setText(me.ipAddressBox, "");
844 me.ipAddressBox.cursorPos = 0;
845 me.ipAddressBoxFocused = -1;
847 void ServerList_ShowEmpty_Click(entity box, entity me)
849 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
850 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
852 me.ipAddressBox.setText(me.ipAddressBox, "");
853 me.ipAddressBox.cursorPos = 0;
854 me.ipAddressBoxFocused = -1;
856 void ServerList_ShowFull_Click(entity box, entity me)
858 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
859 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
861 me.ipAddressBox.setText(me.ipAddressBox, "");
862 me.ipAddressBox.cursorPos = 0;
863 me.ipAddressBoxFocused = -1;
865 void XonoticServerList_setSortOrder(entity me, int fld, int direction)
867 if(me.currentSortField == fld)
868 direction = -me.currentSortOrder;
869 me.currentSortOrder = direction;
870 me.currentSortField = fld;
871 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
872 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
873 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
874 me.sortButton4.forcePressed = 0;
875 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
877 if(me.selectedServer)
878 strunzone(me.selectedServer);
879 me.selectedServer = string_null;
880 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
882 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
884 vector originInLBSpace, sizeInLBSpace;
885 originInLBSpace = eY * (-me.itemHeight);
886 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
888 vector originInDialogSpace, sizeInDialogSpace;
889 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
890 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
892 btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
893 btn.Container_size_x = sizeInDialogSpace.x * theSize;
894 btn.setText(btn, theTitle);
895 btn.onClick = theFunc;
896 btn.onClickEntity = me;
899 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
901 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
903 me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
904 me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
905 me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
907 me.columnIconsOrigin = 0;
908 me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
909 me.columnPingSize = me.realFontSize.x * 3;
910 me.columnMapSize = me.realFontSize.x * 10;
911 me.columnTypeSize = me.realFontSize.x * 4;
912 me.columnPlayersSize = me.realFontSize.x * 5;
913 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
914 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
915 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
916 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
917 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
918 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
920 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
921 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
922 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
923 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
924 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
926 int f = me.currentSortField;
929 me.currentSortField = -1;
930 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
933 void ServerList_Connect_Click(entity btn, entity me)
935 localcmd(sprintf("connect %s\n",
936 ((me.ipAddressBox.text != "") ?
937 me.ipAddressBox.text : me.selectedServer
941 void ServerList_Favorite_Click(entity btn, entity me)
944 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
947 m_play_click_sound(MENU_SOUND_SELECT);
948 me.toggleFavorite(me, me.ipAddressBox.text);
949 me.ipAddressBoxFocused = -1;
952 void ServerList_Info_Click(entity btn, entity me)
955 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
957 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
958 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
959 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
961 void XonoticServerList_doubleClickListBoxItem(entity me, int i, vector where)
963 ServerList_Connect_Click(NULL, me);
965 void XonoticServerList_drawListBoxItem(entity me, int i, vector absSize, bool isSelected)
967 // layout: Ping, Server name, Map name, NP, TP, MP
974 int freeslots = -1, sflags = -1, j, m;
975 string s, typestr, versionstr, k, v, modname;
977 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
979 vector oldscale = draw_scale;
980 vector oldshift = draw_shift;
981 #define SET_YRANGE(start,end) \
982 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
983 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
985 for (j = 0; j < category_draw_count; ++j) {
986 // Matches exactly the headings with increased height.
987 if (i == category_item[j])
991 if (j < category_draw_count)
993 entity catent = RetrieveCategoryEnt(category_name[j]);
997 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
998 me.categoriesHeight / (me.categoriesHeight + 1)
1001 eY * me.realUpperMargin
1004 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1007 eX * (me.columnNameOrigin),
1008 strcat(catent.cat_string, ":"),
1011 SKINCOLOR_SERVERLIST_CATEGORY,
1012 SKINALPHA_SERVERLIST_CATEGORY,
1015 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1020 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1022 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1023 m = tokenizebyseparator(s, ":");
1028 versionstr = argv(1);
1032 for(j = 2; j < m; ++j)
1036 k = substring(argv(j), 0, 1);
1037 v = substring(argv(j), 1, -1);
1041 freeslots = stof(v);
1048 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1050 modname = "Xonotic";
1054 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1055 s = gethostcachestring(SLIST_FIELD_MOD, i);
1057 if(modname == "Xonotic")
1061 // list the mods here on which the pure server check actually works
1062 if(modname != "Xonotic")
1063 if(modname != "InstaGib" || modname != "MinstaGib")
1064 if(modname != "CTS")
1065 if(modname != "NIX")
1066 if(modname != "NewToys")
1069 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1070 theAlpha = SKINALPHA_SERVERLIST_FULL;
1071 else if(freeslots == 0)
1072 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1073 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1074 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1078 p = gethostcachenumber(SLIST_FIELD_PING, i);
1079 const int PING_LOW = 75;
1080 const int PING_MED = 200;
1081 const int PING_HIGH = 500;
1083 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1084 else if(p < PING_MED)
1085 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1086 else if(p < PING_HIGH)
1088 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1089 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1094 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1097 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1099 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1100 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1103 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1105 isv4 = isv6 = false;
1106 if(substring(s, 0, 1) == "[")
1111 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1117 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1118 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1120 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1121 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1126 if(cvar("crypto_aeslevel") >= 2)
1131 if(cvar("crypto_aeslevel") >= 1)
1141 // 2: AES recommended but not available
1142 // 3: AES possible and will be used
1143 // 4: AES recommended and will be used
1149 vector iconSize = '0 0 0';
1150 iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
1151 iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
1153 vector iconPos = '0 0 0';
1154 iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
1155 iconPos_y = (1 - iconSize.y) * 0.5;
1159 if (!(me.seenIPv4 && me.seenIPv6))
1161 iconPos.x += iconSize.x * 0.5;
1163 else if(me.seenIPv4 && me.seenIPv6)
1167 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1169 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1171 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1172 iconPos.x += iconSize.x;
1177 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1178 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1180 iconPos.x += iconSize.x;
1182 if(modname == "Xonotic")
1186 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1187 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1192 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1193 if(draw_PictureSize(n) == '0 0 0')
1194 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1196 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1198 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1200 iconPos.x += iconSize.x;
1202 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1204 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1205 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1207 iconPos.x += iconSize.x;
1215 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1218 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1219 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1222 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1223 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1226 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1227 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1229 // server playercount
1230 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1231 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1234 bool XonoticServerList_keyDown(entity me, int scan, bool ascii, bool shift)
1238 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1239 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1241 if(scan == K_ENTER || scan == K_KP_ENTER)
1243 ServerList_Connect_Click(NULL, me);
1246 else if(scan == K_MOUSE2 || scan == K_SPACE)
1250 m_play_click_sound(MENU_SOUND_OPEN);
1251 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1252 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1257 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1261 me.toggleFavorite(me, me.selectedServer);
1262 me.ipAddressBoxFocused = -1;
1267 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1269 else if(!me.controlledTextbox)
1272 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1275 float XonoticServerList_getTotalHeight(entity me)
1277 float num_normal_rows = me.nItems;
1278 int num_headers = category_draw_count;
1279 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1281 int XonoticServerList_getItemAtPos(entity me, float pos)
1283 pos = pos / me.itemHeight;
1285 for (i = category_draw_count - 1; i >= 0; --i) {
1286 int itemidx = category_item[i];
1287 float itempos = i * me.categoriesHeight + category_item[i];
1288 if (pos >= itempos + me.categoriesHeight + 1)
1289 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1293 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1296 float XonoticServerList_getItemStart(entity me, int item)
1299 for (i = category_draw_count - 1; i >= 0; --i) {
1300 int itemidx = category_item[i];
1301 float itempos = i * me.categoriesHeight + category_item[i];
1302 if (item >= itemidx + 1)
1303 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1304 if (item >= itemidx)
1305 return itempos * me.itemHeight;
1307 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1308 return item * me.itemHeight;
1310 float XonoticServerList_getItemHeight(entity me, int item)
1313 for (i = 0; i < category_draw_count; ++i) {
1314 // Matches exactly the headings with increased height.
1315 if (item == category_item[i])
1316 return me.itemHeight * (me.categoriesHeight + 1);
1318 return me.itemHeight;