]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/menu/xonotic/serverlist.c
Fix some things
[xonotic/xonotic-data.pk3dir.git] / qcsrc / menu / xonotic / serverlist.c
index 9e49626023375b96651e5fa13bc4c2e50d0f6c54..0a43f2009a9517ff2e9b9aa51b0692d865c2824d 100644 (file)
@@ -7,6 +7,7 @@ CLASS(XonoticServerList) EXTENDS(XonoticListBox)
        METHOD(XonoticServerList, clickListBoxItem, void(entity, float, vector))
        METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
        METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
+       METHOD(XonoticServerList, toggleFavorite, void(entity, string))
 
        ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
 
@@ -35,7 +36,7 @@ CLASS(XonoticServerList) EXTENDS(XonoticListBox)
        ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
        ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
        ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
-       METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: 0 = just reparametrize, 1 = send new requests, 2 = clear
+       METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
        ATTRIB(XonoticServerList, needsRefresh, float, 1)
        METHOD(XonoticServerList, focusEnter, void(entity))
        METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
@@ -48,6 +49,7 @@ CLASS(XonoticServerList) EXTENDS(XonoticListBox)
        ATTRIB(XonoticServerList, infoButton, entity, NULL)
        ATTRIB(XonoticServerList, currentSortOrder, float, 0)
        ATTRIB(XonoticServerList, currentSortField, float, -1)
+       ATTRIB(XonoticServerList, lastBumpSelectTime, float, 0)
        ATTRIB(XonoticServerList, lastClickedServer, float, -1)
        ATTRIB(XonoticServerList, lastClickedTime, float, 0)
 
@@ -57,7 +59,62 @@ CLASS(XonoticServerList) EXTENDS(XonoticListBox)
        ATTRIB(XonoticServerList, seenIPv6, float, 0)
 ENDCLASS(XonoticServerList)
 entity makeXonoticServerList();
+
+#ifndef IMPLEMENTATION
+var float autocvar_menu_slist_categories = TRUE;
+var float autocvar_menu_slist_categories_onlyifmultiple = TRUE; 
+var float autocvar_menu_slist_purethreshold = 10;
+var string autocvar_menu_slist_recommended = "76.124.107.5:26004";
+
+// server cache fields
+#define SLIST_FIELDS \
+       SLIST_FIELD(CNAME,       "cname") \
+       SLIST_FIELD(PING,        "ping") \
+       SLIST_FIELD(GAME,        "game") \
+       SLIST_FIELD(MOD,         "mod") \
+       SLIST_FIELD(MAP,         "map") \
+       SLIST_FIELD(NAME,        "name") \
+       SLIST_FIELD(MAXPLAYERS,  "maxplayers") \
+       SLIST_FIELD(NUMPLAYERS,  "numplayers") \
+       SLIST_FIELD(NUMHUMANS,   "numhumans") \
+       SLIST_FIELD(NUMBOTS,     "numbots") \
+       SLIST_FIELD(PROTOCOL,    "protocol") \
+       SLIST_FIELD(FREESLOTS,   "freeslots") \
+       SLIST_FIELD(PLAYERS,     "players") \
+       SLIST_FIELD(QCSTATUS,    "qcstatus") \
+       SLIST_FIELD(CATEGORY,    "category") \
+       SLIST_FIELD(ISFAVORITE,  "isfavorite")
+
+#define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
+SLIST_FIELDS
+#undef SLIST_FIELD
+
+// sort flags
+const float SLSF_DESCENDING = 1;
+const float SLSF_FAVORITES = 2;
+const float SLSF_CATEGORIES = 4;
+
+const float REFRESHSERVERLIST_RESORT = 0;    // sort the server list again to update for changes to e.g. favorite status, categories
+const float REFRESHSERVERLIST_REFILTER = 1;  // ..., also update filter and sort criteria
+const float REFRESHSERVERLIST_ASK = 2;       // ..., also suggest querying servers now
+const float REFRESHSERVERLIST_RESET = 3;     // ..., also clear the list first
+
+// function declarations
+float Get_Cat_Num_FromString(string input);
+entity Get_Cat_Ent(float catnum);
+
+float IsServerInList(string list, string srv);
+#define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
+#define IsRecommended(srv) IsServerInList(cvar_string("menu_slist_recommended"), srv) // todo: use update notification instead of cvar
+
+float CheckCategoryOverride(float cat);
+float CheckCategoryForEntry(float entry); 
+float m_getserverlistentrycategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
+
+void RegisterSLCategories();
+
 void ServerList_Connect_Click(entity btn, entity me);
+void ServerList_Categories_Click(entity box, entity me);
 void ServerList_ShowEmpty_Click(entity box, entity me);
 void ServerList_ShowFull_Click(entity box, entity me);
 void ServerList_Filter_Change(entity box, entity me);
@@ -65,44 +122,109 @@ void ServerList_Favorite_Click(entity btn, entity me);
 void ServerList_Info_Click(entity btn, entity me);
 void ServerList_Update_favoriteButton(entity btn, entity me);
 
-float SLIST_FIELD_CNAME;
-float SLIST_FIELD_PING;
-float SLIST_FIELD_GAME;
-float SLIST_FIELD_MOD;
-float SLIST_FIELD_MAP;
-float SLIST_FIELD_NAME;
-float SLIST_FIELD_MAXPLAYERS;
-float SLIST_FIELD_NUMPLAYERS;
-float SLIST_FIELD_NUMHUMANS;
-float SLIST_FIELD_NUMBOTS;
-float SLIST_FIELD_PROTOCOL;
-float SLIST_FIELD_FREESLOTS;
-float SLIST_FIELD_PLAYERS;
-float SLIST_FIELD_QCSTATUS;
-float SLIST_FIELD_ISFAVORITE;
-#endif
+// fields for category entities
+#define MAX_CATEGORIES 9
+#define CATEGORY_FIRST 1
+entity categories[MAX_CATEGORIES];
+float category_ent_count;
+.string cat_name;
+.string cat_string;
+.string cat_enoverride_string;
+.string cat_dioverride_string;
+.float cat_enoverride;
+.float cat_dioverride;
+
+// fields for drawing categories
+float category_name[MAX_CATEGORIES];
+float category_item[MAX_CATEGORIES];
+float category_draw_count;
+
+#define SLIST_CATEGORIES \
+       SLIST_CATEGORY(CAT_FAVORITED,    "",            "",             _("Favorites")) \
+       SLIST_CATEGORY(CAT_RECOMMENDED,  "",            "CAT_SERVERS",  _("Recommended")) \
+       SLIST_CATEGORY(CAT_NORMAL,       "",            "CAT_SERVERS",  _("Normal Servers")) \
+       SLIST_CATEGORY(CAT_SERVERS,      "CAT_NORMAL",  "CAT_SERVERS",  _("Servers")) \
+       SLIST_CATEGORY(CAT_XPM,          "CAT_NORMAL",  "CAT_SERVERS",  _("Competitive Mode")) \
+       SLIST_CATEGORY(CAT_MODIFIED,     "",            "CAT_SERVERS",  _("Modified Servers")) \
+       SLIST_CATEGORY(CAT_OVERKILL,     "",            "CAT_SERVERS",  _("Overkill Mode")) \
+       SLIST_CATEGORY(CAT_MINSTAGIB,    "",            "CAT_SERVERS",  _("MinstaGib Mode")) \
+       SLIST_CATEGORY(CAT_DEFRAG,       "",            "CAT_SERVERS",  _("Defrag Mode"))
+
+#define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
+#define SLIST_CATEGORY(name,enoverride,dioverride,str) \
+       float name; \
+       var string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
+SLIST_CATEGORIES
+#undef SLIST_CATEGORY
 
+#endif
+#endif
 #ifdef IMPLEMENTATION
-void ServerList_UpdateFieldIDs()
-{
-       SLIST_FIELD_CNAME = gethostcacheindexforkey( "cname" );
-       SLIST_FIELD_PING = gethostcacheindexforkey( "ping" );
-       SLIST_FIELD_GAME = gethostcacheindexforkey( "game" );
-       SLIST_FIELD_MOD = gethostcacheindexforkey( "mod" );
-       SLIST_FIELD_MAP = gethostcacheindexforkey( "map" );
-       SLIST_FIELD_NAME = gethostcacheindexforkey( "name" );
-       SLIST_FIELD_MAXPLAYERS = gethostcacheindexforkey( "maxplayers" );
-       SLIST_FIELD_NUMPLAYERS = gethostcacheindexforkey( "numplayers" );
-       SLIST_FIELD_NUMHUMANS = gethostcacheindexforkey( "numhumans" );
-       SLIST_FIELD_NUMBOTS = gethostcacheindexforkey( "numbots" );
-       SLIST_FIELD_PROTOCOL = gethostcacheindexforkey( "protocol" );
-       SLIST_FIELD_FREESLOTS = gethostcacheindexforkey( "freeslots" );
-       SLIST_FIELD_PLAYERS = gethostcacheindexforkey( "players" );
-       SLIST_FIELD_QCSTATUS = gethostcacheindexforkey( "qcstatus" );
-       SLIST_FIELD_ISFAVORITE = gethostcacheindexforkey( "isfavorite" );
-}
-
-float IsFavorite(string srv)
+
+void RegisterSLCategories()
+{
+       entity cat;
+       #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
+               SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
+               CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
+               cat = spawn(); \
+               categories[name - 1] = cat; \
+               cat.classname = "slist_category"; \
+               cat.cat_name = strzone(#name); \
+               cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
+               cat.cat_dioverride_string = strzone(dioverride); \
+               cat.cat_string = strzone(str);
+       SLIST_CATEGORIES
+       #undef SLIST_CATEGORY
+
+       float i, catnum;
+       string s;
+
+       #define PROCESS_OVERRIDE(override_string,override_field) \
+               for(i = 0; i < category_ent_count; ++i) \
+               { \
+                       s = categories[i].override_string; \
+                       if((s != "") && (s != categories[i].cat_name)) \
+                       { \
+                               catnum = Get_Cat_Num_FromString(s); \
+                               if(catnum) \
+                               { \
+                                       strunzone(categories[i].override_string); \
+                                       categories[i].override_field = catnum; \
+                                       continue; \
+                               } \
+                       } \
+                       strunzone(categories[i].override_string); \
+                       categories[i].override_field = 0; \
+               }
+       PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
+       PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
+       #undef PROCESS_OVERRIDE
+}
+
+// Supporting Functions
+float Get_Cat_Num_FromString(string input)
+{
+       float i;
+       for(i = 0; i < category_ent_count; ++i) { if(categories[i].cat_name == input) { return (i + 1); } }
+       print(sprintf("Get_Cat_Num_FromString('%s'): Improper category name!\n", input));
+       return 0;
+}
+entity Get_Cat_Ent(float catnum)
+{
+       if((catnum > 0) && (catnum <= category_ent_count))
+       {
+               return categories[catnum - 1];
+       }
+       else
+       {
+               error(sprintf("Get_Cat_Ent(%d): Improper category number!\n", catnum));
+               return world;
+       }
+}
+
+
+float IsServerInList(string list, string srv)
 {
        string p;
        float i, n;
@@ -112,7 +234,7 @@ float IsFavorite(string srv)
        if(srv == "")
                return FALSE;
        p = crypto_getidfp(srv);
-       n = tokenize_console(cvar_string("net_slist_favorites"));
+       n = tokenize_console(list);
        for(i = 0; i < n; ++i)
        {
                if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
@@ -130,7 +252,95 @@ float IsFavorite(string srv)
        return FALSE;
 }
 
-void ToggleFavorite(string srv)
+float CheckCategoryOverride(float cat)
+{
+       entity catent = Get_Cat_Ent(cat);
+       if(catent)
+       {
+               float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride); 
+               if(override) { return override; }
+               else { return cat; }
+       }
+       else
+       {
+               error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
+               return cat;
+       }
+}
+
+float CheckCategoryForEntry(float entry)
+{
+       string s, k, v, modtype = "";
+       float j, m, impure = 0;
+       s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
+       m = tokenizebyseparator(s, ":");
+
+       for(j = 2; j < m; ++j)
+       {
+               if(argv(j) == "")
+                       break;
+               k = substring(argv(j), 0, 1);
+               v = substring(argv(j), 1, -1);
+               if(k == "P") { impure = stof(v); }
+               else if(k == "M") { modtype = strtolower(v); }
+       }
+
+       //print(sprintf("modtype = %s\n", modtype)); 
+
+       if(impure > autocvar_menu_slist_purethreshold) { impure = TRUE; }
+       else { impure = FALSE; }
+
+       if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
+       if(IsRecommended(gethostcachestring(SLIST_FIELD_CNAME, entry))) { return CAT_RECOMMENDED; }
+       else if(modtype != "xonotic")
+       {
+               switch(modtype)
+               {
+                       // old servers which don't report their mod name are considered modified now
+                       case "": { return CAT_MODIFIED; }
+                       
+                       case "xpm": { return CAT_XPM; } 
+                       case "minstagib": { return CAT_MINSTAGIB; }
+                       case "overkill": { return CAT_OVERKILL; }
+
+                       // "cts" is allowed as compat, xdf is replacement
+                       case "cts": 
+                       case "xdf": { return CAT_DEFRAG; }
+                       
+                       //if(modname != "CTS")
+                       //if(modname != "NIX")
+                       //if(modname != "NewToys")
+                       
+                       default: { dprint(sprintf("Found strange mod type: %s\n", modtype)); return CAT_MODIFIED; }
+               }
+       }
+       else { return (impure ? CAT_MODIFIED : CAT_NORMAL); }
+
+       // should never hit this point
+       error(sprintf("CheckCategoryForEntry(%d): Function fell through without normal return!\n", entry));
+       return FALSE;
+}
+
+float XonoticServerList_MapItems(float num)
+{
+       float i, n;
+
+       if not(category_draw_count) { return num; } // there are no categories to process
+
+       for(i = 0, n = 1; n <= category_draw_count; ++i, ++n)
+       {
+               //print(sprintf("num: %d, i: %d, category_draw_count: %d, category_item[i]: %d\n", num, i, category_draw_count, category_item[i])); 
+               if(category_item[i] == (num - i)) { /*print("inserting cat... \\/\n");*/ return -category_name[i]; }
+               else if(n == category_draw_count) { /*print("end item... \\/\n");*/ return (num - n); }
+               else if((num - i) <= category_item[n]) { /*print("next item... \\/\n");*/ return (num - n); }
+       }
+
+       // should never hit this point
+       error("wtf XonoticServerList_MapItems fail?");
+       return FALSE;
+}
+
+void XonoticServerList_toggleFavorite(entity me, string srv)
 {
        string s, s0, s1, s2, srv_resolved, p;
        float i, n, f;
@@ -177,15 +387,16 @@ void ToggleFavorite(string srv)
                        cvar_set("net_slist_favorites", strcat(s, s1, srv));
        }
 
-       resorthostcache();
+       me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
 }
 
 void ServerList_Update_favoriteButton(entity btn, entity me)
 {
-       if(IsFavorite(me.ipAddressBox.text))
-               me.favoriteButton.setText(me.favoriteButton, _("Remove"));
-       else
-               me.favoriteButton.setText(me.favoriteButton, _("Bookmark"));
+       me.favoriteButton.setText(me.favoriteButton,
+               (IsFavorite(me.ipAddressBox.text) ?
+                       _("Remove") : _("Bookmark")
+               )
+       );
 }
 
 entity makeXonoticServerList()
@@ -199,13 +410,21 @@ void XonoticServerList_configureXonoticServerList(entity me)
 {
        me.configureXonoticListBox(me);
 
-       ServerList_UpdateFieldIDs();
+       // update field ID's
+       #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
+       SLIST_FIELDS
+       #undef SLIST_FIELD
 
+       // clear list
        me.nItems = 0;
+
+       // build categories
+       RegisterSLCategories();
 }
 void XonoticServerList_setSelected(entity me, float i)
 {
-       float save;
+       // todo: add logic to skip categories
+       float save, num;
        save = me.selectedItem;
        SUPER(XonoticServerList).setSelected(me, i);
        /*
@@ -214,33 +433,68 @@ void XonoticServerList_setSelected(entity me, float i)
        */
        if(me.nItems == 0)
                return;
-       if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
-               return; // sorry, it would be wrong
 
-       if(me.selectedServer)
-               strunzone(me.selectedServer);
-       me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
+       //if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != XonoticServerList_MapItems(me.nItems))
+       //      { error("^1XonoticServerList_setSelected(); ERROR: ^7Host cache viewcount mismatches nItems!\n"); return; } // sorry, it would be wrong
+       // ^ todo: make this work somehow?
 
-       me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
-       me.ipAddressBox.cursorPos = strlen(me.selectedServer);
-       me.ipAddressBoxFocused = -1;
+       #define SET_SELECTED_SERVER(cachenum) \
+               if(me.selectedServer) { strunzone(me.selectedServer); } \
+               me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, cachenum)); \
+               me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer); \
+               me.ipAddressBox.cursorPos = strlen(me.selectedServer); \
+               me.ipAddressBoxFocused = -1; \
+               return;
+
+       num = XonoticServerList_MapItems(me.selectedItem);
+
+       if(num >= 0) { SET_SELECTED_SERVER(num); }
+       else if(save > me.selectedItem)
+       {
+               if(me.selectedItem == 0) { return; }
+               else
+               {
+                       if(me.lastClickedTime >= me.lastBumpSelectTime)
+                       {
+                               SUPER(XonoticServerList).setSelected(me, me.selectedItem - 1);
+                               num = XonoticServerList_MapItems(me.selectedItem);
+                               if(num >= 0)
+                               {
+                                       me.lastBumpSelectTime = time;
+                                       SET_SELECTED_SERVER(num);
+                               }
+                       }
+               }
+       }
+       else if(save < me.selectedItem)
+       {
+               if(me.selectedItem == me.nItems) { return; }
+               else
+               {
+                       if(me.lastClickedTime >= me.lastBumpSelectTime)
+                       {
+                               SUPER(XonoticServerList).setSelected(me, me.selectedItem + 1);
+                               num = XonoticServerList_MapItems(me.selectedItem);
+                               if(num >= 0)
+                               {
+                                       me.lastBumpSelectTime = time;
+                                       SET_SELECTED_SERVER(num);
+                               }
+                       }
+               }
+       }
 }
+
 void XonoticServerList_refreshServerList(entity me, float mode)
 {
-       // 0: just reparametrize
-       // 1: also ask for new servers
-       // 2: clear
        //print("refresh of type ", ftos(mode), "\n");
-       /* if(mode == 2) // borken
-       {
-               // clear list
-               localcmd("net_slist\n");
-               me.needsRefresh = 1; // net_slist kills sort order, so we need to restore it later
-       }
-       else */
+
+       if(mode >= REFRESHSERVERLIST_REFILTER)
        {
-               float m, o, i, n; // moin moin
+               float m, i, n;
+               float listflags = 0;
                string s, typestr, modstr;
+
                s = me.filterString;
 
                m = strstrofs(s, ":", 0);
@@ -300,14 +554,17 @@ void XonoticServerList_refreshServerList(entity me, float mode)
                        sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
                        sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
                }
-               o = 2; // favorites first
-               if(me.currentSortOrder < 0)
-                       o |= 1; // descending
-               sethostcachesort(me.currentSortField, o);
-               resorthostcache();
-               if(mode >= 1)
-                       refreshhostcache();
+
+               // sorting flags
+               //listflags |= SLSF_FAVORITES;
+               listflags |= SLSF_CATEGORIES;
+               if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
+               sethostcachesort(me.currentSortField, listflags);
        }
+       
+       resorthostcache();
+       if(mode >= REFRESHSERVERLIST_ASK)
+               refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
 }
 void XonoticServerList_focusEnter(entity me)
 {
@@ -317,11 +574,12 @@ void XonoticServerList_focusEnter(entity me)
                return;
        }
        me.nextRefreshTime = time + 10;
-       me.refreshServerList(me, 1);
+       me.refreshServerList(me, REFRESHSERVERLIST_ASK);
 }
+
 void XonoticServerList_draw(entity me)
 {
-       float i, found, owned;
+       float i, found, owned, num;
 
        if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
        {
@@ -333,7 +591,7 @@ void XonoticServerList_draw(entity me)
        if(me.currentSortField == -1)
        {
                me.setSortOrder(me, SLIST_FIELD_PING, +1);
-               me.refreshServerList(me, 2);
+               me.refreshServerList(me, REFRESHSERVERLIST_RESET);
        }
        else if(me.needsRefresh == 1)
        {
@@ -342,12 +600,59 @@ void XonoticServerList_draw(entity me)
        else if(me.needsRefresh == 2)
        {
                me.needsRefresh = 0;
-               me.refreshServerList(me, 0);
+               me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
        }
 
        owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
 
-       me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
+       for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
+       category_draw_count = 0;
+
+       if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
+       {
+               float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
+               me.nItems = itemcount;
+               
+               //float visible = floor(me.scrollPos / me.itemHeight);
+               // ^ unfortunately no such optimization can be made-- we must process through the
+               // entire list, otherwise there is no way to know which item is first in its category.
+
+               float cat, x;
+               for(i = 0; i < itemcount; ++i)
+               {
+                       cat = gethostcachenumber(SLIST_FIELD_CATEGORY, i);
+                       if(cat)
+                       {
+                               if(category_draw_count == 0)
+                               {
+                                       category_name[category_draw_count] = cat;
+                                       category_item[category_draw_count] = i;
+                                       ++category_draw_count;
+                                       ++me.nItems;
+                               }
+                               else
+                               {
+                                       found = 0;
+                                       for(x = 0; x < category_draw_count; ++x) { if(cat == category_name[x]) { found = 1; } }
+                                       if not(found)
+                                       {
+                                               category_name[category_draw_count] = cat;
+                                               category_item[category_draw_count] = i;
+                                               ++category_draw_count;
+                                               ++me.nItems;
+                                       }
+                               }
+                       }
+               }
+               if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
+               {
+                       category_name[0] = category_name[1] = -1;
+                       category_item[0] = category_item[1] = -1;
+                       category_draw_count = 0;
+                       me.nItems = itemcount;
+               }
+       }
+       else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
 
        me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
        me.infoButton.disabled = ((me.nItems == 0) || !owned);
@@ -357,27 +662,35 @@ void XonoticServerList_draw(entity me)
        if(me.selectedServer)
        {
                for(i = 0; i < me.nItems; ++i)
-                       if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
+               {
+                       num = XonoticServerList_MapItems(i);
+                       if(num >= 0)
                        {
-                               if(i != me.selectedItem)
+                               if(gethostcachestring(SLIST_FIELD_CNAME, num) == me.selectedServer)
                                {
-                                       me.lastClickedServer = -1;
-                                       me.selectedItem = i;
+                                       if(i != me.selectedItem)
+                                       {
+                                               me.lastClickedServer = -1;
+                                               me.selectedItem = i;
+                                       }
+                                       found = 1;
+                                       break;
                                }
-                               found = 1;
-                               break;
                        }
+               }
        }
        if(!found)
+       {
                if(me.nItems > 0)
                {
-                       if(me.selectedItem >= me.nItems)
-                               me.selectedItem = me.nItems - 1;
-                       if(me.selectedServer)
-                               strunzone(me.selectedServer);
-                       me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
-               }
+                       if(me.selectedItem >= me.nItems) { me.selectedItem = me.nItems - 1; }
+                       if(me.selectedServer) { strunzone(me.selectedServer); }
 
+                       num = XonoticServerList_MapItems(me.selectedItem);
+                       if(num >= 0) { me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, num)); }
+               }
+       }
+       
        if(owned)
        {
                if(me.selectedServer != me.ipAddressBox.text)
@@ -467,7 +780,23 @@ void ServerList_Filter_Change(entity box, entity me)
                me.filterString = strzone(box.text);
        else
                me.filterString = string_null;
-       me.refreshServerList(me, 0);
+       me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
+
+       me.ipAddressBox.setText(me.ipAddressBox, "");
+       me.ipAddressBox.cursorPos = 0;
+       me.ipAddressBoxFocused = -1;
+}
+void ServerList_Categories_Click(entity box, entity me)
+{
+       box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
+       ///refreshhostcache(TRUE);
+
+       //cvar_set("net_slist_pause", "0");
+       //Destroy_Category_Entities();
+       //CALL_ACCUMULATED_FUNCTION(RegisterSLCategories);
+       //me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
+
+       me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
 
        me.ipAddressBox.setText(me.ipAddressBox, "");
        me.ipAddressBox.cursorPos = 0;
@@ -476,7 +805,7 @@ void ServerList_Filter_Change(entity box, entity me)
 void ServerList_ShowEmpty_Click(entity box, entity me)
 {
        box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
-       me.refreshServerList(me, 0);
+       me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 
        me.ipAddressBox.setText(me.ipAddressBox, "");
        me.ipAddressBox.cursorPos = 0;
@@ -485,7 +814,7 @@ void ServerList_ShowEmpty_Click(entity box, entity me)
 void ServerList_ShowFull_Click(entity box, entity me)
 {
        box.setChecked(box, me.filterShowFull = !me.filterShowFull);
-       me.refreshServerList(me, 0);
+       me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 
        me.ipAddressBox.setText(me.ipAddressBox, "");
        me.ipAddressBox.cursorPos = 0;
@@ -506,7 +835,7 @@ void XonoticServerList_setSortOrder(entity me, float fld, float direction)
        if(me.selectedServer)
                strunzone(me.selectedServer);
        me.selectedServer = string_null;
-       me.refreshServerList(me, 0);
+       me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 }
 void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
 {
@@ -562,10 +891,11 @@ void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize,
 }
 void ServerList_Connect_Click(entity btn, entity me)
 {
-       if(me.ipAddressBox.text == "")
-               localcmd("connect ", me.selectedServer, "\n");
-       else
-               localcmd("connect ", me.ipAddressBox.text, "\n");
+       localcmd(sprintf("connect %s\n",
+               ((me.ipAddressBox.text != "") ?
+                       me.ipAddressBox.text : me.selectedServer
+               )
+       ));
 }
 void ServerList_Favorite_Click(entity btn, entity me)
 {
@@ -573,25 +903,29 @@ void ServerList_Favorite_Click(entity btn, entity me)
        ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
        if(ipstr != "")
        {
-               ToggleFavorite(me.ipAddressBox.text);
+               me.toggleFavorite(me, me.ipAddressBox.text);
                me.ipAddressBoxFocused = -1;
        }
 }
 void ServerList_Info_Click(entity btn, entity me)
 {
-       main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
+       main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, XonoticServerList_MapItems(me.selectedItem));
        DialogOpenButton_Click(me, main.serverInfoDialog);
 }
 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
 {
-       if(i == me.lastClickedServer)
-               if(time < me.lastClickedTime + 0.3)
-               {
-                       // DOUBLE CLICK!
-                       ServerList_Connect_Click(NULL, me);
-               }
-       me.lastClickedServer = i;
-       me.lastClickedTime = time;
+       float num = XonoticServerList_MapItems(i);
+       if(num >= 0)
+       {
+               if(num == me.lastClickedServer)
+                       if(time < me.lastClickedTime + 0.3)
+                       {
+                               // DOUBLE CLICK!
+                               ServerList_Connect_Click(NULL, me);
+                       }
+               me.lastClickedServer = num;
+               me.lastClickedTime = time;
+       }
 }
 void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 {
@@ -603,10 +937,32 @@ void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float
        float m, pure, freeslots, j, sflags;
        string s, typestr, versionstr, k, v, modname;
 
+       float item = XonoticServerList_MapItems(i);
+       //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
+       
+       if(item < 0)
+       {
+               entity catent = Get_Cat_Ent(-item);
+               if(catent)
+               {
+                       draw_Text(
+                               eY * me.realUpperMargin
+                               +
+                               eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
+                               catent.cat_string,
+                               me.realFontSize,
+                               '1 1 1',
+                               SKINALPHA_TEXT,
+                               0
+                       );
+                       return;
+               }
+       }
+       
        if(isSelected)
                draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 
-       s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
+       s = gethostcachestring(SLIST_FIELD_QCSTATUS, item);
        m = tokenizebyseparator(s, ":");
        typestr = "";
        if(m >= 2)
@@ -641,7 +997,7 @@ void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float
 
        /*
        SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
-       s = gethostcachestring(SLIST_FIELD_MOD, i);
+       s = gethostcachestring(SLIST_FIELD_MOD, item);
        if(s != "data")
                if(modname == "Xonotic")
                        modname = s;
@@ -655,16 +1011,16 @@ void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float
        if(modname != "NewToys")
                pure = 0;
 
-       if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
+       if(gethostcachenumber(SLIST_FIELD_FREESLOTS, item) <= 0)
                theAlpha = SKINALPHA_SERVERLIST_FULL;
        else if(freeslots == 0)
                theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
-       else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
+       else if not(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item))
                theAlpha = SKINALPHA_SERVERLIST_EMPTY;
        else
                theAlpha = 1;
 
-       p = gethostcachenumber(SLIST_FIELD_PING, i);
+       p = gethostcachenumber(SLIST_FIELD_PING, item);
 #define PING_LOW 75
 #define PING_MED 200
 #define PING_HIGH 500
@@ -683,13 +1039,13 @@ void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float
                theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
        }
 
-       if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
+       if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, item))
        {
                theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
                theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
        }
 
-       s = gethostcachestring(SLIST_FIELD_CNAME, i);
+       s = gethostcachestring(SLIST_FIELD_CNAME, item);
 
        isv4 = isv6 = 0;
        if(substring(s, 0, 1) == "[")
@@ -732,88 +1088,104 @@ void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float
        // 4: AES recommended and will be used
        // 5: AES required
 
-       {
-               vector iconSize = '0 0 0';
-               iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
-               iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
-
-               vector iconPos = '0 0 0';
-               iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
-               iconPos_y = (1 - iconSize_y) * 0.5;
+       // --------------
+       //  RENDER ICONS
+       // --------------
+       vector iconSize = '0 0 0';
+       iconSize_y = me.realFontSize_y * me.iconsSizeFactor;
+       iconSize_x = me.realFontSize_x * me.iconsSizeFactor;
 
-               string n;
+       vector iconPos = '0 0 0';
+       iconPos_x = (me.columnIconsSize - 3 * iconSize_x) * 0.5;
+       iconPos_y = (1 - iconSize_y) * 0.5;
 
-               if not(me.seenIPv4 && me.seenIPv6)
-               {
-                       iconPos_x += iconSize_x * 0.5;
-               }
-               else if(me.seenIPv4 && me.seenIPv6)
-               {
-                       n = string_null;
-                       if(isv6)
-                               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
-                       else if(isv4)
-                               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
-                       if(n)
-                               draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
-                       iconPos_x += iconSize_x;
-               }
+       string n;
 
-               if(q > 0)
-               {
-                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
+       if not(me.seenIPv4 && me.seenIPv6)
+       {
+               iconPos_x += iconSize_x * 0.5;
+       }
+       else if(me.seenIPv4 && me.seenIPv6)
+       {
+               n = string_null;
+               if(isv6)
+                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
+               else if(isv4)
+                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
+               if(n)
                        draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
-               }
                iconPos_x += iconSize_x;
+       }
 
-               if(modname == "Xonotic")
-               {
-                       if(pure == 0)
-                       {
-                               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
-                               draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
-                       }
-               }
-               else
-               {
-                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
-                       if(draw_PictureSize(n) == '0 0 0')
-                               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
-                       if(pure == 0)
-                               draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
-                       else
-                               draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
-               }
-               iconPos_x += iconSize_x;
+       if(q > 0)
+       {
+               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
+               draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
+       }
+       iconPos_x += iconSize_x;
 
-               if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
+       if(modname == "Xonotic")
+       {
+               if(pure == 0)
                {
-                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
+                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
                        draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
                }
-               iconPos_x += iconSize_x;
        }
+       else
+       {
+               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
+               if(draw_PictureSize(n) == '0 0 0')
+                       draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
+               if(pure == 0)
+                       draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
+               else
+                       draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
+       }
+       iconPos_x += iconSize_x;
 
+       if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
+       {
+               draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
+               draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
+       }
+       iconPos_x += iconSize_x;
+       
+       // --------------
+       //  RENDER TEXT
+       // --------------
+       
+       // ping
        s = ftos(p);
        draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
-       s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
+
+       // server name
+       s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, item), me.columnNameSize, 0, me.realFontSize);
        draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
-       s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
+
+       // server map
+       s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, item), me.columnMapSize, 0, me.realFontSize);
        draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
+
+       // server gametype
        s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
        draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
-       s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
+
+       // server playercount
+       s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, item)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, item)));
        draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
 }
 
 float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
 {
-       float i;
+       float i = XonoticServerList_MapItems(me.selectedItem);
        vector org, sz;
 
        org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
        sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
 
+       me.lastBumpSelectTime = 0;
+
        if(scan == K_ENTER || scan == K_KP_ENTER)
        {
                ServerList_Connect_Click(NULL, me);
@@ -821,9 +1193,9 @@ float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
        }
        else if(scan == K_MOUSE2 || scan == K_SPACE)
        {
-               if(me.nItems != 0)
+               if((me.nItems != 0) && (i >= 0))
                {
-                       main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
+                       main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, i);
                        DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
                        return 1;
                }
@@ -831,10 +1203,9 @@ float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
        }
        else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
        {
-               i = me.selectedItem;
-               if(i < me.nItems)
+               if((me.nItems != 0) && (i >= 0))
                {
-                       ToggleFavorite(me.selectedServer);
+                       me.toggleFavorite(me, me.selectedServer);
                        me.ipAddressBoxFocused = -1;
                        return 1;
                }