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);
959 DialogOpenButton_Click(me, main.serverInfoDialog);
961 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
963 if(i == me.lastClickedServer)
964 if(time < me.lastClickedTime + 0.3)
967 ServerList_Connect_Click(NULL, me);
969 me.lastClickedServer = i;
970 me.lastClickedTime = time;
972 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
974 // layout: Ping, Server name, Map name, NP, TP, MP
979 float m, pure, freeslots, j, sflags;
980 string s, typestr, versionstr, k, v, modname;
982 //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
984 vector oldscale = draw_scale;
985 vector oldshift = draw_shift;
986 #define SET_YRANGE(start,end) \
987 draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
988 draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
990 for (j = 0; j < category_draw_count; ++j) {
991 // Matches exactly the headings with increased height.
992 if (i == category_item[j])
996 if (j < category_draw_count)
998 entity catent = RetrieveCategoryEnt(category_name[j]);
1002 (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
1003 me.categoriesHeight / (me.categoriesHeight + 1)
1006 eY * me.realUpperMargin
1009 eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
1012 eX * (me.columnNameOrigin),
1013 strcat(catent.cat_string, ":"),
1020 SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
1025 draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
1027 s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
1028 m = tokenizebyseparator(s, ":");
1033 versionstr = argv(1);
1039 for(j = 2; j < m; ++j)
1043 k = substring(argv(j), 0, 1);
1044 v = substring(argv(j), 1, -1);
1048 freeslots = stof(v);
1055 #ifdef COMPAT_NO_MOD_IS_XONOTIC
1057 modname = "Xonotic";
1061 SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
1062 s = gethostcachestring(SLIST_FIELD_MOD, i);
1064 if(modname == "Xonotic")
1068 // list the mods here on which the pure server check actually works
1069 if(modname != "Xonotic")
1070 if(modname != "MinstaGib")
1071 if(modname != "CTS")
1072 if(modname != "NIX")
1073 if(modname != "NewToys")
1076 if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
1077 theAlpha = SKINALPHA_SERVERLIST_FULL;
1078 else if(freeslots == 0)
1079 theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
1080 else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
1081 theAlpha = SKINALPHA_SERVERLIST_EMPTY;
1085 p = gethostcachenumber(SLIST_FIELD_PING, i);
1087 #define PING_MED 200
1088 #define PING_HIGH 500
1090 theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
1091 else if(p < PING_MED)
1092 theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
1093 else if(p < PING_HIGH)
1095 theColor = SKINCOLOR_SERVERLIST_HIGHPING;
1096 theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
1101 theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
1104 if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
1106 theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
1107 theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
1110 s = gethostcachestring(SLIST_FIELD_CNAME, i);
1113 if(substring(s, 0, 1) == "[")
1118 else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
1124 q = stof(substring(crypto_getencryptlevel(s), 0, 1));
1125 if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
1127 theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
1128 theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
1133 if(cvar("crypto_aeslevel") >= 2)
1138 if(cvar("crypto_aeslevel") >= 1)
1148 // 2: AES recommended but not available
1149 // 3: AES possible and will be used
1150 // 4: AES recommended and will be used
1156 vector iconSize = '0 0 0';
1157 iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
1158 iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
1160 vector iconPos = '0 0 0';
1161 iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
1162 iconPos_y = (1 - iconSize_y) * 0.5;
1166 if (!(me.seenIPv4 && me.seenIPv6))
1168 iconPos_x += iconSize_x * 0.5;
1170 else if(me.seenIPv4 && me.seenIPv6)
1174 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
1176 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
1178 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1179 iconPos_x += iconSize_x;
1184 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
1185 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1187 iconPos_x += iconSize_x;
1189 if(modname == "Xonotic")
1193 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
1194 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1199 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
1200 if(draw_PictureSize(n) == '0 0 0')
1201 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
1203 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1205 draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
1207 iconPos_x += iconSize_x;
1209 if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
1211 draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
1212 draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
1214 iconPos_x += iconSize_x;
1222 draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1225 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
1226 draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
1229 s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
1230 draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1233 s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
1234 draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1236 // server playercount
1237 s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
1238 draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
1241 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
1245 org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
1246 sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
1248 if(scan == K_ENTER || scan == K_KP_ENTER)
1250 ServerList_Connect_Click(NULL, me);
1253 else if(scan == K_MOUSE2 || scan == K_SPACE)
1257 main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
1258 DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
1263 else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
1267 me.toggleFavorite(me, me.selectedServer);
1268 me.ipAddressBoxFocused = -1;
1273 else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
1275 else if(!me.controlledTextbox)
1278 return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
1281 float XonoticServerList_getTotalHeight(entity me) {
1282 float num_normal_rows = me.nItems;
1283 float num_headers = category_draw_count;
1284 return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
1286 float XonoticServerList_getItemAtPos(entity me, float pos) {
1287 pos = pos / me.itemHeight;
1289 for (i = category_draw_count - 1; i >= 0; --i) {
1290 float itemidx = category_item[i];
1291 float itempos = i * me.categoriesHeight + category_item[i];
1292 if (pos >= itempos + me.categoriesHeight + 1)
1293 return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
1297 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1300 float XonoticServerList_getItemStart(entity me, float item) {
1302 for (i = category_draw_count - 1; i >= 0; --i) {
1303 float itemidx = category_item[i];
1304 float itempos = i * me.categoriesHeight + category_item[i];
1305 if (item >= itemidx + 1)
1306 return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
1307 if (item >= itemidx)
1308 return itempos * me.itemHeight;
1310 // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
1311 return item * me.itemHeight;
1313 float XonoticServerList_getItemHeight(entity me, float item) {
1315 for (i = 0; i < category_draw_count; ++i) {
1316 // Matches exactly the headings with increased height.
1317 if (item == category_item[i])
1318 return me.itemHeight * (me.categoriesHeight + 1);
1320 return me.itemHeight;