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, lastClickedServer, float, -1)
53 ATTRIB(XonoticServerList, lastClickedTime, float, 0)
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 #define MAX_CATEGORIES 9
132 #define CATEGORY_FIRST 1
133 entity categories[MAX_CATEGORIES];
134 float 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 float category_name[MAX_CATEGORIES];
144 float category_item[MAX_CATEGORIES];
145 float 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_MINSTAGIB, "", "CAT_SERVERS", ZCTX(_("SLCAT^MinstaGib 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 var 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(float 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 float 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 float CheckCategoryOverride(float cat)
267 entity catent = RetrieveCategoryEnt(cat);
270 float 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 float CheckCategoryForEntry(float entry)
283 string s, k, v, modtype = "";
284 float 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; }
365 case "minstagib": { return CAT_MINSTAGIB; }
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;
386 srv_resolved = netaddress_resolve(srv, 26000);
387 p = crypto_getidfp(srv_resolved);
388 s = cvar_string("net_slist_favorites");
389 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 = spawnXonoticServerList();
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, float i)
463 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, float mode)
484 //print("refresh of type ", ftos(mode), "\n");
486 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 if(time < me.nextRefreshTime)
567 //print("sorry, no refresh yet\n");
570 me.nextRefreshTime = time + 10;
571 me.refreshServerList(me, REFRESHSERVERLIST_ASK);
574 void XonoticServerList_draw(entity me)
576 float i, found, owned;
578 if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
582 _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
585 if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
589 _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
592 if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
596 _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
599 if(me.currentSortField == -1)
601 me.setSortOrder(me, SLIST_FIELD_PING, +1);
602 me.refreshServerList(me, REFRESHSERVERLIST_RESET);
604 else if(me.needsRefresh == 1)
606 me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
608 else if(me.needsRefresh == 2)
611 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
613 else if(me.needsRefresh == 3)
616 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
619 owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
621 for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
622 category_draw_count = 0;
624 if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
626 float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
627 me.nItems = itemcount;
629 //float visible = floor(me.scrollPos / me.itemHeight);
630 // ^ unfortunately no such optimization can be made-- we must process through the
631 // entire list, otherwise there is no way to know which item is first in its category.
633 // binary search method suggested by div
636 for(x = 1; x <= category_ent_count; ++x) {
638 float last = (itemcount - 1);
643 float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
644 float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
646 // The first one is already > x.
647 // Therefore, category x does not exist.
648 // Higher numbered categories do exist though.
649 } else if (catl < x) {
650 // The last one is < x.
651 // Thus this category - and any following -
654 } else if (catf == x) {
655 // Starts at first. This breaks the loop
656 // invariant in the binary search and thus has
657 // to be handled separately.
658 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
659 error("Category mismatch I");
661 if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
662 error("Category mismatch II");
663 category_name[category_draw_count] = x;
664 category_item[category_draw_count] = first;
665 ++category_draw_count;
668 // At this point, catf <= x < catl, thus
669 // catf < catl, thus first < last.
672 // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
673 // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
676 while (last - first > 1) {
677 float middle = floor((first + last) / 2);
678 // By loop condition, middle != first && middle != last.
679 float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
689 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
690 error("Category mismatch III");
692 if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
693 error("Category mismatch IV");
694 category_name[category_draw_count] = x;
695 category_item[category_draw_count] = last;
696 ++category_draw_count;
697 begin = last + 1; // already scanned through these, skip 'em
700 begin = last; // already scanned through these, skip 'em
703 if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
705 category_name[0] = -1;
706 category_item[0] = -1;
707 category_draw_count = 0;
708 me.nItems = itemcount;
711 else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
713 me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
714 me.infoButton.disabled = ((me.nItems == 0) || !owned);
715 me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
718 if(me.selectedServer)
720 for(i = 0; i < me.nItems; ++i)
722 if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
724 if(i != me.selectedItem)
726 me.lastClickedServer = -1;
738 if(me.selectedItem >= me.nItems)
739 me.selectedItem = me.nItems - 1;
740 if(me.selectedServer)
741 strunzone(me.selectedServer);
742 me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
748 if(me.selectedServer != me.ipAddressBox.text)
750 me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
751 me.ipAddressBox.cursorPos = strlen(me.selectedServer);
752 me.ipAddressBoxFocused = -1;
756 if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
758 if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
759 ServerList_Update_favoriteButton(NULL, me);
760 me.ipAddressBoxFocused = me.ipAddressBox.focused;
763 SUPER(XonoticServerList).draw(me);
765 void ServerList_PingSort_Click(entity btn, entity me)
767 me.setSortOrder(me, SLIST_FIELD_PING, +1);
769 void ServerList_NameSort_Click(entity btn, entity me)
771 me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
773 void ServerList_MapSort_Click(entity btn, entity me)
775 me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
777 void ServerList_PlayerSort_Click(entity btn, entity me)
779 me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
781 void ServerList_TypeSort_Click(entity btn, entity me)
786 m = strstrofs(s, ":", 0);
789 s = substring(s, 0, m);
790 while(substring(s, m+1, 1) == " ") // skip spaces
796 for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
798 t = MapInfo_Type_ToString(i);
800 if(t == "") // it repeats (default case)
803 // choose the first one
804 s = MapInfo_Type_ToString(1);
809 // the type was found
810 // choose the next one
811 s = MapInfo_Type_ToString(i * 2);
813 s = MapInfo_Type_ToString(1);
820 s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
822 me.controlledTextbox.setText(me.controlledTextbox, s);
823 me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
824 me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
825 //ServerList_Filter_Change(me.controlledTextbox, me);
827 void ServerList_Filter_Change(entity box, entity me)
830 strunzone(me.filterString);
832 me.filterString = strzone(box.text);
834 me.filterString = string_null;
835 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
837 me.ipAddressBox.setText(me.ipAddressBox, "");
838 me.ipAddressBox.cursorPos = 0;
839 me.ipAddressBoxFocused = -1;
841 void ServerList_Categories_Click(entity box, entity me)
843 box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
844 me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
846 me.ipAddressBox.setText(me.ipAddressBox, "");
847 me.ipAddressBox.cursorPos = 0;
848 me.ipAddressBoxFocused = -1;
850 void ServerList_ShowEmpty_Click(entity box, entity me)
852 box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
853 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
855 me.ipAddressBox.setText(me.ipAddressBox, "");
856 me.ipAddressBox.cursorPos = 0;
857 me.ipAddressBoxFocused = -1;
859 void ServerList_ShowFull_Click(entity box, entity me)
861 box.setChecked(box, me.filterShowFull = !me.filterShowFull);
862 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
864 me.ipAddressBox.setText(me.ipAddressBox, "");
865 me.ipAddressBox.cursorPos = 0;
866 me.ipAddressBoxFocused = -1;
868 void XonoticServerList_setSortOrder(entity me, float fld, float direction)
870 if(me.currentSortField == fld)
871 direction = -me.currentSortOrder;
872 me.currentSortOrder = direction;
873 me.currentSortField = fld;
874 me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
875 me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
876 me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
877 me.sortButton4.forcePressed = 0;
878 me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
880 if(me.selectedServer)
881 strunzone(me.selectedServer);
882 me.selectedServer = string_null;
883 me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
885 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
887 vector originInLBSpace, sizeInLBSpace;
888 originInLBSpace = eY * (-me.itemHeight);
889 sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
891 vector originInDialogSpace, sizeInDialogSpace;
892 originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
893 sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
895 btn.Container_origin_x = originInDialogSpace_x + sizeInDialogSpace_x * theOrigin;
896 btn.Container_size_x = sizeInDialogSpace_x * theSize;
897 btn.setText(btn, theTitle);
898 btn.onClick = theFunc;
899 btn.onClickEntity = me;
902 void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
904 SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
906 me.realFontSize_y = me.fontSize / (absSize_y * me.itemHeight);
907 me.realFontSize_x = me.fontSize / (absSize_x * (1 - me.controlWidth));
908 me.realUpperMargin = 0.5 * (1 - me.realFontSize_y);
910 me.columnIconsOrigin = 0;
911 me.columnIconsSize = me.realFontSize_x * 4 * me.iconsSizeFactor;
912 me.columnPingSize = me.realFontSize_x * 3;
913 me.columnMapSize = me.realFontSize_x * 10;
914 me.columnTypeSize = me.realFontSize_x * 4;
915 me.columnPlayersSize = me.realFontSize_x * 5;
916 me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize_x;
917 me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize_x;
918 me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize_x;
919 me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize_x;
920 me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize_x;
921 me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize_x;
923 me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
924 me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
925 me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
926 me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
927 me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
930 f = me.currentSortField;
933 me.currentSortField = -1;
934 me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
937 void ServerList_Connect_Click(entity btn, entity me)
939 localcmd(sprintf("connect %s\n",
940 ((me.ipAddressBox.text != "") ?
941 me.ipAddressBox.text : me.selectedServer
945 void ServerList_Favorite_Click(entity btn, entity me)
948 ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
951 me.toggleFavorite(me, me.ipAddressBox.text);
952 me.ipAddressBoxFocused = -1;
955 void ServerList_Info_Click(entity btn, entity me)
958 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
960 vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
961 vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
962 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
964 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
966 if(i == me.lastClickedServer)
967 if(time < me.lastClickedTime + 0.3)
970 ServerList_Connect_Click(NULL, me);
972 me.lastClickedServer = i;
973 me.lastClickedTime = time;
975 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
977 // layout: Ping, Server name, Map name, NP, TP, MP
982 float m, pure, freeslots, j, sflags;
983 string s, typestr, versionstr, k, v, modname;
985 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
987 vector oldscale = draw_scale;
988 vector oldshift = draw_shift;
989 #define SET_YRANGE(start,end) \
990 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
991 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
993 for (j = 0; j < category_draw_count; ++j) {
994 // Matches exactly the headings with increased height.
995 if (i == category_item[j])
999 if (j < category_draw_count)
1001 entity catent = RetrieveCategoryEnt(category_name[j]);
1005 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1006 me.categoriesHeight / (me.categoriesHeight + 1)
1009 eY * me.realUpperMargin
1012 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1015 eX * (me.columnNameOrigin),
1016 strcat(catent.cat_string, ":"),
1019 SKINCOLOR_SERVERLIST_CATEGORY,
1020 SKINALPHA_SERVERLIST_CATEGORY,
1023 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1028 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1030 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1031 m = tokenizebyseparator(s, ":");
1036 versionstr = argv(1);
1042 for(j = 2; j < m; ++j)
1046 k = substring(argv(j), 0, 1);
1047 v = substring(argv(j), 1, -1);
1051 freeslots = stof(v);
1058 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1060 modname = "Xonotic";
1064 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1065 s = gethostcachestring(SLIST_FIELD_MOD, i);
1067 if(modname == "Xonotic")
1071 // list the mods here on which the pure server check actually works
1072 if(modname != "Xonotic")
1073 if(modname != "MinstaGib")
1074 if(modname != "CTS")
1075 if(modname != "NIX")
1076 if(modname != "NewToys")
1079 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1080 theAlpha = SKINALPHA_SERVERLIST_FULL;
1081 else if(freeslots == 0)
1082 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1083 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1084 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1088 p = gethostcachenumber(SLIST_FIELD_PING, i);
1090 #define PING_MED 200
1091 #define PING_HIGH 500
1093 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1094 else if(p < PING_MED)
1095 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1096 else if(p < PING_HIGH)
1098 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1099 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1104 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1107 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1109 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1110 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1113 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1116 if(substring(s, 0, 1) == "[")
1121 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1127 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1128 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1130 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1131 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1136 if(cvar("crypto_aeslevel") >= 2)
1141 if(cvar("crypto_aeslevel") >= 1)
1151 // 2: AES recommended but not available
1152 // 3: AES possible and will be used
1153 // 4: AES recommended and will be used
1159 vector iconSize = '0 0 0';
1160 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1161 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1163 vector iconPos = '0 0 0';
1164 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1165 iconPos_y = (1 - iconSize_y) * 0.5;
1169 if (!(me.seenIPv4 && me.seenIPv6))
1171 iconPos_x += iconSize_x * 0.5;
1173 else if(me.seenIPv4 && me.seenIPv6)
1177 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1179 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1181 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1182 iconPos_x += iconSize_x;
1187 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1188 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1190 iconPos_x += iconSize_x;
1192 if(modname == "Xonotic")
1196 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1197 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1202 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1203 if(draw_PictureSize(n) == '0 0 0')
1204 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1206 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1208 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1210 iconPos_x += iconSize_x;
1212 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1214 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1215 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1217 iconPos_x += iconSize_x;
1225 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1228 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1229 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1232 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1233 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1236 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1237 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1239 // server playercount
1240 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1241 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1244 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1248 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1249 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1251 if(scan == K_ENTER || scan == K_KP_ENTER)
1253 ServerList_Connect_Click(NULL, me);
1256 else if(scan == K_MOUSE2 || scan == K_SPACE)
1260 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1261 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1266 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1270 me.toggleFavorite(me, me.selectedServer);
1271 me.ipAddressBoxFocused = -1;
1276 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1278 else if(!me.controlledTextbox)
1281 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1284 float XonoticServerList_getTotalHeight(entity me) {
1285 float num_normal_rows = me.nItems;
1286 float num_headers = category_draw_count;
1287 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1289 float XonoticServerList_getItemAtPos(entity me, float pos) {
1290 pos = pos / me.itemHeight;
1292 for (i = category_draw_count - 1; i >= 0; --i) {
1293 float itemidx = category_item[i];
1294 float itempos = i * me.categoriesHeight + category_item[i];
1295 if (pos >= itempos + me.categoriesHeight + 1)
1296 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1300 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1303 float XonoticServerList_getItemStart(entity me, float item) {
1305 for (i = category_draw_count - 1; i >= 0; --i) {
1306 float itemidx = category_item[i];
1307 float itempos = i * me.categoriesHeight + category_item[i];
1308 if (item >= itemidx + 1)
1309 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1310 if (item >= itemidx)
1311 return itempos * me.itemHeight;
1313 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1314 return item * me.itemHeight;
1316 float XonoticServerList_getItemHeight(entity me, float item) {
1318 for (i = 0; i < category_draw_count; ++i) {
1319 // Matches exactly the headings with increased height.
1320 if (item == category_item[i])
1321 return me.itemHeight * (me.categoriesHeight + 1);
1323 return me.itemHeight;