Merge remote-tracking branch 'origin/samual/serverlist'
authorSamual Lenks <samual@xonotic.org>
Sun, 24 Nov 2013 04:55:13 +0000 (23:55 -0500)
committerSamual Lenks <samual@xonotic.org>
Sun, 24 Nov 2013 04:55:13 +0000 (23:55 -0500)
* origin/samual/serverlist: (63 commits)
  Use new cvar check URL; kill CVAR_POPCON.
  Show recommended servers when categories are disabled too
  Add a compatibility stop date to the update notification info.
  Fix my breaking of emergency pk3's :D
  ARGH, that really was a STUPID bug.
  Make APPEND_STRING behave "sane".
  Some cleanup
  Properly handle the cvars for serverlist categories now
  Support promoted servers in the recommendations
  Switch to new format for updatenotification
  Switch to recommendation mode 3
  Rename "Bookmark" button to "Favorite" button (it's called favorites...)
  Adjustments to category rendering- left align with colon over servername
  Simplify a LOT.
  Refactor item position code. Should be easier to adapt to new category handling.
  ONE BYTE.
  Fix bug in begin handling.
  Bottom align the headings.
  Give the headings 1.5 rows height.
  use a proper binary search
  ...

Conflicts:
qcsrc/menu/xonotic/serverlist.c
qcsrc/menu/xonotic/util.qc

defaultXonotic.cfg
qcsrc/Makefile
qcsrc/common/util.qh
qcsrc/dpdefs/menudefs.qc
qcsrc/menu/menu.qc
qcsrc/menu/xonotic/dialog_multiplayer_join.c
qcsrc/menu/xonotic/serverlist.c
qcsrc/menu/xonotic/util.qc
qcsrc/menu/xonotic/util.qh

index 39b43139339b93a5589fecb6bab5c83a731f56bb..dd16de9bc6ee4224b900bf576cfc86ff8e5fce20 100644 (file)
@@ -978,6 +978,28 @@ seta menu_slist_showfull 1 "show servers even if they are full and have no slots
 seta menu_slist_showempty 1 "show servers even if they are no empty and have no opponents to play against"
 seta menu_slist_modfilter "" // set to either: !modname or modname. modname of = means "same as we are running now".
 
+// other serverlist cvars
+seta menu_slist_categories 1
+seta menu_slist_categories_onlyifmultiple 1
+seta menu_slist_purethreshold 0
+seta menu_slist_modimpurity 0
+seta menu_slist_recommendations 3
+seta menu_slist_recommendations_maxping 150
+seta menu_slist_recommendations_minfreeslots 1
+seta menu_slist_recommendations_minhumans 0
+seta menu_slist_recommendations_purethreshold -1
+
+// serverlist category override cvars
+seta menu_slist_categories_CAT_FAVORITED_override ""
+seta menu_slist_categories_CAT_RECOMMENDED_override ""
+seta menu_slist_categories_CAT_NORMAL_override ""
+seta menu_slist_categories_CAT_SERVERS_override "CAT_NORMAL"
+seta menu_slist_categories_CAT_XPM_override "CAT_NORMAL"
+seta menu_slist_categories_CAT_MODIFIED_override ""
+seta menu_slist_categories_CAT_OVERKILL_override ""
+seta menu_slist_categories_CAT_MINSTAGIB_override ""
+seta menu_slist_categories_CAT_DEFRAG_override ""
+
 seta menu_weaponarena ""
 
 seta menu_maxplayers 16 "maxplayers value when the menu starts a game"
index a10ccd2fd11f5ed318068b0c60e18807f0c14495..833787328efe69d58f57cf868a9e798e184dca32 100644 (file)
@@ -1,6 +1,6 @@
 SCM := $(shell if [ -d .svn ]; then echo svn; elif [ -d ../.git ]; then echo git; fi)
 PERL ?= perl
-QCCFLAGS_WATERMARK ?= -DWATERMARK='"$(shell git describe)"' -DCVAR_POPCON=1
+QCCFLAGS_WATERMARK ?= -DWATERMARK='"$(shell git describe)"'
 QCC ?= gmqcc
 
 QCCVERSIONFILE := qccversion.$(shell $(QCC) --version > qccversion.txt && git hash-object qccversion.txt)
index 66b0edb3dfd64d3a3c312a6884cea413cee93cda..a9f9095c7a40492e6615258364cc0ca059c08620 100644 (file)
@@ -52,7 +52,7 @@ void ACCUMULATE_call(string func)
 // used for simplifying ACCUMULATE_FUNCTIONs
 #define SET_FIRST_OR_LAST(input,first,count) if(!input) { input = (first + count); }
 #define SET_FIELD_COUNT(field,first,count) if(!field) { field = (first + count); ++count; }
-#define CHECK_MAX_COUNT(name,max,count,type) if(count == max) { error(strcat("Maximum ", type, " hit: ", #name, ": ", ftos(count), ".\n")); }
+#define CHECK_MAX_COUNT(name,max,count,type) if(count > max) { error(strcat("Maximum ", type, " hit: ", #name, ": ", ftos(count), ".\n")); }
 
 // this returns a tempstring containing a copy of s with additional \n newlines added, it also replaces \n in the text with a real newline
 // NOTE: s IS allowed to be a tempstring
index 21093868ece7d0b93e290587c1c24637e0657368..0d6c253709540b5f4f82ce8a9d50d7472a2dd21f 100644 (file)
@@ -18,6 +18,7 @@ void(float keynr, float ascii) m_keydown;
 void(float width, float height) m_draw;
 void(float mode) m_toggle;
 void() m_shutdown;
+// optional: float(float) m_gethostcachecategory;
 
 /////////////////////////////////////////////////////////
 // sys constants
@@ -562,8 +563,11 @@ void       resethostcachemasks(void) = #615;
 void   sethostcachemaskstring(float mask, float fld, string str, float op) = #616;
 void   sethostcachemasknumber(float mask, float fld, float num, float op) = #617;
 void   resorthostcache(void) = #618;
-void   sethostcachesort(float fld, float descending) = #619;
-void   refreshhostcache(void) = #620;
+float SLSF_DESCENDING = 1;
+float SLSF_FAVORITES = 2;
+float SLSF_CATEGORIES = 4;
+void   sethostcachesort(float fld, float slsf) = #619;
+void   refreshhostcache(...) = #620;  // optional boolean argument "clear_list"
 float  gethostcachenumber(float fld, float hostnr) = #621;
 float  gethostcacheindexforkey(string key) = #622;
 void   addwantedhostcachekey(string key) = #623;
index d5eff6e4935d89f7309b8e3856e0bfece0f1c13b..1398e5757e9277cdadbecaaed942742b2bb61c3d 100644 (file)
@@ -77,6 +77,8 @@ void m_init()
        CALL_ACCUMULATED_FUNCTION(RegisterWeapons);
        CALL_ACCUMULATED_FUNCTION(RegisterGametypes);
 
+       RegisterSLCategories();
+
        float ddsload = cvar("r_texture_dds_load");
        float texcomp = cvar("gl_texturecompression");
        updateCompression();
index 02d3b410209a0af2bdf371309a45ea7325f57906..91326ad2d5fd7498111e19c88b09588d8226623e 100644 (file)
@@ -26,10 +26,13 @@ void XonoticServerListTab_fill(entity me)
 
        me.TR(me);
                me.TD(me, 1, 0.4, e = makeXonoticTextLabel(0, _("Filter:")));
-               me.TD(me, 1, me.columns - 0.6 * 3 - 0.4, e = makeXonoticInputBox(0, string_null));
+               me.TD(me, 1, me.columns - 0.6 * 3 - 0.9 - 0.4, e = makeXonoticInputBox(0, string_null));
                        e.onChange = ServerList_Filter_Change;
                        e.onChangeEntity = slist;
                        slist.controlledTextbox = e;
+               me.TD(me, 1, 0.9, e = makeXonoticCheckBox(0, "menu_slist_categories", ZCTX(_("SRVS^Categories"))));
+                       e.onClickEntity = slist;
+                       e.onClick = ServerList_Categories_Click;
                me.TD(me, 1, 0.6, e = makeXonoticCheckBox(0, "menu_slist_showempty", ZCTX(_("SRVS^Empty"))));
                        slist.filterShowEmpty = e.checked;
                        e.onClickEntity = slist;
index cb6b0fbcdc5641366c363a25b6159dfd9c3cdfa5..e2a0e6c1249f401847c51e27b40ebef94bd935cd 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)))
@@ -55,10 +56,70 @@ CLASS(XonoticServerList) EXTENDS(XonoticListBox)
 
        ATTRIB(XonoticServerList, seenIPv4, float, 0)
        ATTRIB(XonoticServerList, seenIPv6, float, 0)
+       ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
+
+       METHOD(XonoticServerList, getTotalHeight, float(entity))
+       METHOD(XonoticServerList, getItemAtPos, float(entity, float))
+       METHOD(XonoticServerList, getItemStart, float(entity, float))
+       METHOD(XonoticServerList, getItemHeight, float(entity, float))
 ENDCLASS(XonoticServerList)
 entity makeXonoticServerList();
 
+#ifndef IMPLEMENTATION
+float autocvar_menu_slist_categories;
+float autocvar_menu_slist_categories_onlyifmultiple; 
+float autocvar_menu_slist_purethreshold;
+float autocvar_menu_slist_modimpurity;
+float autocvar_menu_slist_recommendations;
+float autocvar_menu_slist_recommendations_maxping;
+float autocvar_menu_slist_recommendations_minfreeslots; 
+float autocvar_menu_slist_recommendations_minhumans;
+float autocvar_menu_slist_recommendations_purethreshold; 
+
+// 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
+
+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 IsServerInList(string list, string srv);
+#define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
+#define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
+#define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
+
+entity RetrieveCategoryEnt(float catnum);
+
+float CheckCategoryOverride(float cat);
+float CheckCategoryForEntry(float entry); 
+float m_gethostcachecategory(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);
@@ -66,47 +127,114 @@ void ServerList_Favorite_Click(entity btn, entity me);
 void ServerList_Info_Click(entity btn, entity me);
 void ServerList_Update_favoriteButton(entity btn, entity me);
 
-#ifndef IMPLEMENTATION
-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,    "",            "",             ZCTX(_("SLCAT^Favorites"))) \
+       SLIST_CATEGORY(CAT_RECOMMENDED,  "",            "",             ZCTX(_("SLCAT^Recommended"))) \
+       SLIST_CATEGORY(CAT_NORMAL,       "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Normal Servers"))) \
+       SLIST_CATEGORY(CAT_SERVERS,      "CAT_NORMAL",  "CAT_SERVERS",  ZCTX(_("SLCAT^Servers"))) \
+       SLIST_CATEGORY(CAT_XPM,          "CAT_NORMAL",  "CAT_SERVERS",  ZCTX(_("SLCAT^Competitive Mode"))) \
+       SLIST_CATEGORY(CAT_MODIFIED,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Modified Servers"))) \
+       SLIST_CATEGORY(CAT_OVERKILL,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Overkill Mode"))) \
+       SLIST_CATEGORY(CAT_MINSTAGIB,    "",            "CAT_SERVERS",  ZCTX(_("SLCAT^MinstaGib Mode"))) \
+       SLIST_CATEGORY(CAT_DEFRAG,       "",            "CAT_SERVERS",  ZCTX(_("SLCAT^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()
+
+void RegisterSLCategories()
 {
-       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)
+       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, x, 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 = 0; \
+                               for(x = 0; x < category_ent_count; ++x) \
+                               { if(categories[x].cat_name == s) { \
+                                       catnum = (x+1); \
+                                       break; \
+                               } } \
+                               if(catnum) \
+                               { \
+                                       strunzone(categories[i].override_string); \
+                                       categories[i].override_field = catnum; \
+                                       continue; \
+                               } \
+                               else \
+                               { \
+                                       print(sprintf( \
+                                               "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
+                                               s, \
+                                               categories[i].cat_name \
+                                       )); \
+                               } \
+                       } \
+                       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
+entity RetrieveCategoryEnt(float catnum)
+{
+       if((catnum > 0) && (catnum <= category_ent_count))
+       {
+               return categories[catnum - 1];
+       }
+       else
+       {
+               error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
+               return world;
+       }
+}
+
+float IsServerInList(string list, string srv)
 {
        string p;
        float i, n;
@@ -116,7 +244,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)
@@ -134,7 +262,124 @@ float IsFavorite(string srv)
        return FALSE;
 }
 
-void ToggleFavorite(string srv)
+float CheckCategoryOverride(float cat)
+{
+       entity catent = RetrieveCategoryEnt(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, freeslots = 0, sflags = 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);
+               switch(k)
+               {
+                       case "P": { impure = stof(v); break; }
+                       case "S": { freeslots = stof(v); break; }
+                       case "F": { sflags = stof(v); break; }
+                       case "M": { modtype = strtolower(v); break; }
+               }
+       }
+
+       if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
+
+       // check if this server is favorited
+       if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
+
+       // now check if it's recommended
+       if(autocvar_menu_slist_recommendations)
+       {
+               string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
+               
+               if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
+               else
+               {
+                       float recommended = 0;
+                       if(autocvar_menu_slist_recommendations & 1)
+                       {
+                               if(IsRecommended(cname)) { ++recommended; }
+                               else { --recommended; }
+                       }
+                       if(autocvar_menu_slist_recommendations & 2)
+                       {
+                               if(
+                                       ///// check for minimum free slots
+                                       (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
+                                       
+                                       && // check for purity requirement
+                                       (
+                                               (autocvar_menu_slist_recommendations_purethreshold < 0)
+                                               ||
+                                               (impure <= autocvar_menu_slist_recommendations_purethreshold)
+                                       )
+                                       
+                                       && // check for minimum amount of humans
+                                       (
+                                               gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
+                                               >=
+                                               autocvar_menu_slist_recommendations_minhumans
+                                       )
+                                       
+                                       && // check for maximum latency
+                                       (
+                                               gethostcachenumber(SLIST_FIELD_PING, entry)
+                                               <=
+                                               autocvar_menu_slist_recommendations_maxping
+                                       )
+                               )
+                                       { ++recommended; }
+                               else
+                                       { --recommended; }
+                       }
+                       if(recommended > 0) { return CAT_RECOMMENDED; }
+               }
+       }
+
+       // if not favorited or recommended, check modname
+       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; }
+                       //case "nix": { return CAT_NIX; }
+                       //case "newtoys": { return CAT_NEWTOYS; }
+
+                       // "cts" is allowed as compat, xdf is replacement
+                       case "cts": 
+                       case "xdf": { return CAT_DEFRAG; }
+                       
+                       default: { dprint(sprintf("Found strange mod type: %s\n", modtype)); return CAT_MODIFIED; }
+               }
+       }
+
+       // must be normal or impure server
+       return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
+}
+
+void XonoticServerList_toggleFavorite(entity me, string srv)
 {
        string s, s0, s1, s2, srv_resolved, p;
        float i, n, f;
@@ -181,15 +426,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") : _("Favorite")
+               )
+       );
 }
 
 entity makeXonoticServerList()
@@ -203,8 +449,12 @@ 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;
 }
 void XonoticServerList_setSelected(entity me, float i)
@@ -231,20 +481,14 @@ void XonoticServerList_setSelected(entity me, float i)
 }
 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);
@@ -304,14 +548,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)
 {
@@ -321,8 +568,9 @@ 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;
@@ -334,10 +582,24 @@ void XonoticServerList_draw(entity me)
                _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
        }
 
+       if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
+       {
+               if(!me.needsRefresh)
+                       me.needsRefresh = 3;
+               _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
+       }
+
+       if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
+       {
+               if(!me.needsRefresh)
+                       me.needsRefresh = 3;
+               _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
+       }
+
        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)
        {
@@ -346,12 +608,107 @@ void XonoticServerList_draw(entity me)
        else if(me.needsRefresh == 2)
        {
                me.needsRefresh = 0;
-               me.refreshServerList(me, 0);
+               me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
+       }
+       else if(me.needsRefresh == 3)
+       {
+               me.needsRefresh = 0;
+               me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
        }
 
        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.
+
+               // binary search method suggested by div
+               float x;
+               float begin = 0;
+               for(x = 1; x <= category_ent_count; ++x) {
+                       float first = begin;
+                       float last = (itemcount - 1);
+                       if (first > last) {
+                               // List is empty.
+                               break;
+                       }
+                       float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
+                       float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
+                       if (catf > x) {
+                               // The first one is already > x.
+                               // Therefore, category x does not exist.
+                               // Higher numbered categories do exist though.
+                       } else if (catl < x) {
+                               // The last one is < x.
+                               // Thus this category - and any following -
+                               // don't exist.
+                               break;
+                       } else if (catf == x) {
+                               // Starts at first. This breaks the loop
+                               // invariant in the binary search and thus has
+                               // to be handled separately.
+                               if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
+                                       error("Category mismatch I");
+                               if(first > 0)
+                                       if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
+                                               error("Category mismatch II");
+                               category_name[category_draw_count] = x;
+                               category_item[category_draw_count] = first;
+                               ++category_draw_count;
+                               begin = first + 1;
+                       } else {
+                               // At this point, catf <= x < catl, thus
+                               // catf < catl, thus first < last.
+                               // INVARIANTS:
+                               // last - first >= 1
+                               // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
+                               // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
+                               // catf < x
+                               // catl >= x
+                               while (last - first > 1) {
+                                       float middle = floor((first + last) / 2);
+                                       // By loop condition, middle != first && middle != last.
+                                       float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
+                                       if (cat >= x) {
+                                               last = middle;
+                                               catl = cat;
+                                       } else {
+                                               first = middle;
+                                               catf = cat;
+                                       }
+                               }
+                               if (catl == x) {
+                                       if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
+                                               error("Category mismatch III");
+                                       if(last > 0)
+                                               if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
+                                                       error("Category mismatch IV");
+                                       category_name[category_draw_count] = x;
+                                       category_item[category_draw_count] = last;
+                                       ++category_draw_count;
+                                       begin = last + 1; // already scanned through these, skip 'em
+                               }
+                               else
+                                       begin = last; // already scanned through these, skip 'em
+                       }
+               }
+               if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
+               {
+                       category_name[0] = -1;
+                       category_item[0] = -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);
@@ -361,6 +718,7 @@ void XonoticServerList_draw(entity me)
        if(me.selectedServer)
        {
                for(i = 0; i < me.nItems; ++i)
+               {
                        if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
                        {
                                if(i != me.selectedItem)
@@ -371,8 +729,10 @@ void XonoticServerList_draw(entity me)
                                found = 1;
                                break;
                        }
+               }
        }
        if(!found)
+       {
                if(me.nItems > 0)
                {
                        if(me.selectedItem >= me.nItems)
@@ -381,7 +741,8 @@ void XonoticServerList_draw(entity me)
                                strunzone(me.selectedServer);
                        me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
                }
-
+       }
+       
        if(owned)
        {
                if(me.selectedServer != me.ipAddressBox.text)
@@ -471,7 +832,16 @@ 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);
+       me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
 
        me.ipAddressBox.setText(me.ipAddressBox, "");
        me.ipAddressBox.cursorPos = 0;
@@ -480,7 +850,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;
@@ -489,7 +859,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;
@@ -510,7 +880,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)
 {
@@ -566,10 +936,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)
 {
@@ -577,13 +948,14 @@ 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);
+       if (me.nItems != 0)
+               main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
        DialogOpenButton_Click(me, main.serverInfoDialog);
 }
 void XonoticServerList_clickListBoxItem(entity me, float i, vector where)
@@ -607,6 +979,48 @@ void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float
        float m, pure, freeslots, j, sflags;
        string s, typestr, versionstr, k, v, modname;
 
+       //print(sprintf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems));
+
+       vector oldscale = draw_scale;
+       vector oldshift = draw_shift;
+#define SET_YRANGE(start,end) \
+       draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
+       draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
+
+       for (j = 0; j < category_draw_count; ++j) {
+               // Matches exactly the headings with increased height.
+               if (i == category_item[j])
+                       break;
+       }
+
+       if (j < category_draw_count)
+       {
+               entity catent = RetrieveCategoryEnt(category_name[j]);
+               if(catent)
+               {
+                       SET_YRANGE(
+                               (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
+                               me.categoriesHeight / (me.categoriesHeight + 1)
+                       );
+                       draw_Text(
+                               eY * me.realUpperMargin
+                               +
+#if 0
+                               eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
+                               catent.cat_string,
+#else
+                               eX * (me.columnNameOrigin),
+                               strcat(catent.cat_string, ":"),
+#endif
+                               me.realFontSize,
+                               '1 1 1',
+                               SKINALPHA_TEXT,
+                               0
+                       );
+                       SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
+               }
+       }
+       
        if(isSelected)
                draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 
@@ -736,83 +1150,96 @@ 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 (!(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 (!(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);
+
+       // server name
        s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
        draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
+
+       // server map
        s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), 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);
+
+       // server playercount
        s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
        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;
        vector org, sz;
 
        org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
@@ -835,10 +1262,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)
                {
-                       ToggleFavorite(me.selectedServer);
+                       me.toggleFavorite(me, me.selectedServer);
                        me.ipAddressBoxFocused = -1;
                        return 1;
                }
@@ -851,4 +1277,47 @@ float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
        else
                return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
 }
+
+float XonoticServerList_getTotalHeight(entity me) {
+       float num_normal_rows = me.nItems;
+       float num_headers = category_draw_count;
+       return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
+}
+float XonoticServerList_getItemAtPos(entity me, float pos) {
+       pos = pos / me.itemHeight;
+       float i;
+       for (i = category_draw_count - 1; i >= 0; --i) {
+               float itemidx = category_item[i];
+               float itempos = i * me.categoriesHeight + category_item[i];
+               if (pos >= itempos + me.categoriesHeight + 1)
+                       return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
+               if (pos >= itempos)
+                       return itemidx;
+       }
+       // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
+       return floor(pos);
+}
+float XonoticServerList_getItemStart(entity me, float item) {
+       float i;
+       for (i = category_draw_count - 1; i >= 0; --i) {
+               float itemidx = category_item[i];
+               float itempos = i * me.categoriesHeight + category_item[i];
+               if (item >= itemidx + 1)
+                       return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
+               if (item >= itemidx)
+                       return itempos * me.itemHeight;
+       }
+       // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
+       return item * me.itemHeight;
+}
+float XonoticServerList_getItemHeight(entity me, float item) {
+       float i;
+       for (i = 0; i < category_draw_count; ++i) {
+               // Matches exactly the headings with increased height.
+               if (item == category_item[i])
+                       return me.itemHeight * (me.categoriesHeight + 1);
+       }
+       return me.itemHeight;
+}
+
 #endif
index 2a3b8c2bee436aed9900ec1bb3c3d96282d3e76f..34f67397eb0bef09944f2cc1627facd0cfb668c9 100644 (file)
@@ -284,6 +284,11 @@ void URI_Get_Callback(float id, float status, string data)
        }
 }
 
+void DisableServerBackwardsCompatibility()
+{
+       cvar_set("gameversion_min", ftos(100 * floor(cvar("gameversion") / 100)));
+}
+
 void UpdateNotification_URI_Get_Callback(float id, float status, string data)
 {
        float n;
@@ -314,40 +319,108 @@ void UpdateNotification_URI_Get_Callback(float id, float status, string data)
        else
                n = tokenizebyseparator(data, "\n");
 
-       if(n >= 1)
+       float i;
+       string s; 
+       
+       string un_version = "";
+       string un_download = "";
+       string un_url = "";
+       string un_bannedservers = "";
+       string un_emergency_pk3s = "";
+       string un_promoted = "";
+       string un_recommended = "";
+       string un_compatexpire = "";
+       
+       for(i = 0; i < n; ++i)
        {
-               _Nex_ExtResponseSystem_UpdateTo = argv(0);
-
-               if(vercmp(cvar_string("g_xonoticversion"), _Nex_ExtResponseSystem_UpdateTo) >= 0)
+               s = substring(argv(i), 2, -1);
+               if(s == "") { continue; } // ignore empty lines
+               
+               switch(substring(argv(i), 0, 1))
                {
-                       _Nex_ExtResponseSystem_UpdateTo = ""; // no update needed
-               }
-               else
-               {
-                       // update needed
-                       if(n >= 2)
-                               print(sprintf(_("Update can be downloaded at:\n%s\n"), argv(1)));
-                       if(n >= 3)
-                               _Nex_ExtResponseSystem_UpdateToURL = strzone(argv(2));
+                       #define APPEND_TO_STRING(list,sep,add) ((list) = (((list) != "") ? strcat(list, sep, add) : (add)))
+                       case "V":
+                       {
+                               un_version = s;
+                               break;
+                       }
+                       case "C":
+                       {
+                               un_compatexpire = s;
+                               break;
+                       }
+                       case "D":
+                       {
+                               un_download = s;
+                               break;
+                       }
+                       case "U":
+                       {
+                               un_url = s;
+                               break;
+                       }
+                       case "B":
+                       {
+                               APPEND_TO_STRING(un_bannedservers, " ", s);
+                               break;
+                       }
+                       case "E":
+                       {
+                               if(cvar("menu_updatecheck_getpacks"))
+                                       APPEND_TO_STRING(un_emergency_pk3s, " ", s);
+                               break;
+                       }
+                       case "P":
+                       {
+                               APPEND_TO_STRING(un_promoted, " ", s);
+                               break;
+                       }
+                       case "R":
+                       {
+                               APPEND_TO_STRING(un_recommended, " ", s);
+                               break;
+                       }
                }
+       }
 
-               _Nex_ExtResponseSystem_UpdateTo = strzone(_Nex_ExtResponseSystem_UpdateTo);
-
-               if(n >= 4)
+       if(un_version != "")
+       {
+               if(vercmp(cvar_string("g_xonoticversion"), un_version) < 0)
                {
-                       _Nex_ExtResponseSystem_BannedServers = strzone(argv(3));
-                       _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 1;
+                       // update needed
+                       _Nex_ExtResponseSystem_UpdateTo = strzone(un_version);
+                       if(un_download) { print(sprintf(_("Update can be downloaded at:\n%s\n"), un_download)); }
+                       if(un_url) { _Nex_ExtResponseSystem_UpdateToURL = strzone(un_url); }
+                       DisableServerBackwardsCompatibility();
                }
-
-               if(n >= 5)
+               else if(cvar_string("g_xonoticversion") == un_version)
                {
-                       if(cvar("menu_updatecheck_getpacks"))
+                       if(un_compatexpire != "")
                        {
-                               _Nex_ExtResponseSystem_Packs = strzone(argv(4));
-                               _Nex_ExtResponseSystem_PacksStep = 1;
+                               string curdate = strftime(FALSE, "%Y%m%d%H%M%S");
+                               if (strcmp(curdate, un_compatexpire) >= 0)
+                                       DisableServerBackwardsCompatibility();
                        }
                }
        }
+       
+       if(un_emergency_pk3s != "")
+       {
+               _Nex_ExtResponseSystem_Packs = strzone(un_emergency_pk3s);
+               _Nex_ExtResponseSystem_PacksStep = 1;
+       }
+
+       if(un_promoted != "")
+       {
+               _Nex_ExtResponseSystem_PromotedServers = strzone(un_promoted);
+               _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 1;
+       }
+
+       if(un_recommended != "")
+       {
+               _Nex_ExtResponseSystem_RecommendedServers = strzone(un_recommended);
+               _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 1;
+       }
 }
 
 // END OF URI SYSTEM ////////////////////////////////////////////////////////
@@ -366,36 +439,8 @@ void updateCheck()
 
                        // for privacy, munge the start count a little
                        startcnt = floor((floor(startcnt / 10) + random()) * 10);
-                       uri = sprintf("http://www.xonotic.org/dl/checkupdate.txt?version=%s&cnt=%d", uri_escape(cvar_string("g_xonoticversion")), startcnt);
-
-#ifdef CVAR_POPCON
-                       float cvar_handle, popcon_handle;
-                       float n, i, j;
-                       string k, s;
-                       cvar_handle = buf_create();
-                       buf_cvarlist(cvar_handle, "", "");
-                       n = buf_getsize(cvar_handle);
-                       popcon_handle = buf_create();
-                       for(i= 0, j = 0; i < n; ++i)
-                       {
-                               k = bufstr_get(cvar_handle, i);
-                               if(!(cvar_type(k) & CVAR_TYPEFLAG_SAVED))
-                                       continue;
-                               s = sprintf("%s=%d", uri_escape(k), cvar_string(k) != cvar_defstring(k));
-                               bufstr_set(popcon_handle, j, s);
-                               ++j;
-                       }
-                       buf_del(cvar_handle);
-                       uri_postbuf(
-                               uri, URI_GET_UPDATENOTIFICATION,
-                               "application/x-www-form-urlencoded",
-                               "&",
-                               popcon_handle
-                       );
-                       buf_del(popcon_handle);
-#else
+                       uri = sprintf("http://update.xonotic.org/checkupdate.txt?version=%s&cnt=%d", uri_escape(cvar_string("g_xonoticversion")), startcnt);
                        uri_get(uri, URI_GET_UPDATENOTIFICATION);
-#endif
                }
        }
 
index be13ee289b25607149d3e803621b1234eae0b6cc..3371dd8114637a90a4735b7204c7e59d87047e08 100644 (file)
@@ -45,3 +45,7 @@ void dialog_hudpanel_common_notoggle(entity me, string panelname);
 
 string _Nex_ExtResponseSystem_BannedServers;
 float _Nex_ExtResponseSystem_BannedServersNeedsRefresh;
+string _Nex_ExtResponseSystem_PromotedServers;
+float _Nex_ExtResponseSystem_PromotedServersNeedsRefresh;
+string _Nex_ExtResponseSystem_RecommendedServers;
+float _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh;