/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
Copyright (C) 2000-2009 Darklegion Development
 
This file is part of Tremulous.
 
Tremulous is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
 
Tremulous is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with Tremulous; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

/*
=======================================================================
 
USER INTERFACE MAIN
 
=======================================================================
*/

#include "ui_local.h"

uiInfo_t uiInfo;

static const char *MonthAbbrev[ ] =
{
  "Jan", "Feb", "Mar",
  "Apr", "May", "Jun",
  "Jul", "Aug", "Sep",
  "Oct", "Nov", "Dec"
};

static const char *netSources[ ] =
{
  "Internet",
  "Mplayer",
  "LAN",
  "Favorites"
};

static const int numNetSources = sizeof( netSources ) / sizeof( const char* );

static const char *netnames[ ] =
{
  "???",
  "UDP",
  "IPX",
  NULL
};

/*
================
cvars
================
*/

typedef struct
{
  vmCvar_t  *vmCvar;
  char    *cvarName;
  char    *defaultString;
  int     cvarFlags;
}

cvarTable_t;

vmCvar_t  ui_browserShowFull;
vmCvar_t  ui_browserShowEmpty;

vmCvar_t  ui_dedicated;
vmCvar_t  ui_netSource;
vmCvar_t  ui_selectedMap;
vmCvar_t  ui_lastServerRefresh_0;
vmCvar_t  ui_lastServerRefresh_1;
vmCvar_t  ui_lastServerRefresh_2;
vmCvar_t  ui_lastServerRefresh_3;
vmCvar_t  ui_lastServerRefresh_0_time;
vmCvar_t  ui_lastServerRefresh_1_time;
vmCvar_t  ui_lastServerRefresh_2_time;
vmCvar_t  ui_lastServerRefresh_3_time;
vmCvar_t  ui_smallFont;
vmCvar_t  ui_bigFont;
vmCvar_t  ui_findPlayer;
vmCvar_t  ui_serverStatusTimeOut;
vmCvar_t  ui_textWrapCache;
vmCvar_t  ui_developer;

vmCvar_t  ui_emoticons;
vmCvar_t  ui_winner;
vmCvar_t  ui_chatCommands;

static cvarTable_t    cvarTable[ ] =
{
  { &ui_browserShowFull, "ui_browserShowFull", "1", CVAR_ARCHIVE },
  { &ui_browserShowEmpty, "ui_browserShowEmpty", "1", CVAR_ARCHIVE },

  { &ui_dedicated, "ui_dedicated", "0", CVAR_ARCHIVE },
  { &ui_netSource, "ui_netSource", "0", CVAR_ARCHIVE },
  { &ui_selectedMap, "ui_selectedMap", "0", CVAR_ARCHIVE },
  { &ui_lastServerRefresh_0, "ui_lastServerRefresh_0", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_1, "ui_lastServerRefresh_1", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_2, "ui_lastServerRefresh_2", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_3, "ui_lastServerRefresh_3", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_0, "ui_lastServerRefresh_0_time", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_1, "ui_lastServerRefresh_1_time", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_2, "ui_lastServerRefresh_2_time", "", CVAR_ARCHIVE},
  { &ui_lastServerRefresh_3, "ui_lastServerRefresh_3_time", "", CVAR_ARCHIVE},
  { &ui_smallFont, "ui_smallFont", "0.2", CVAR_ARCHIVE | CVAR_LATCH },
  { &ui_bigFont, "ui_bigFont", "0.5", CVAR_ARCHIVE | CVAR_LATCH },
  { &ui_findPlayer, "ui_findPlayer", "", CVAR_ARCHIVE},
  { &ui_serverStatusTimeOut, "ui_serverStatusTimeOut", "7000", CVAR_ARCHIVE},
  { &ui_textWrapCache, "ui_textWrapCache", "1", CVAR_ARCHIVE },
  { &ui_developer, "ui_developer", "0", CVAR_ARCHIVE | CVAR_CHEAT },
  { &ui_emoticons, "cg_emoticons", "1", CVAR_LATCH | CVAR_ARCHIVE },
  { &ui_winner, "ui_winner", "", CVAR_ROM },
  { &ui_chatCommands, "ui_chatCommands", "1", CVAR_ARCHIVE }
};

static int    cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] );

/*
================
vmMain
 
This is the only way control passes into the module.
This must be the very first function compiled into the .qvm file
================
*/
void UI_Init( qboolean );
void UI_Shutdown( void );
void UI_KeyEvent( int key, qboolean down );
void UI_MouseEvent( int dx, int dy );
int UI_MousePosition( void );
void UI_SetMousePosition( int x, int y );
void UI_Refresh( int realtime );
qboolean UI_IsFullscreen( void );
void UI_SetActiveMenu( uiMenuCommand_t menu );
Q_EXPORT intptr_t vmMain( int command, int arg0, int arg1, int arg2, int arg3,
                          int arg4, int arg5, int arg6, int arg7,
                          int arg8, int arg9, int arg10, int arg11  )
{
  switch( command )
  {
    case UI_GETAPIVERSION:
      return UI_API_VERSION;

    case UI_INIT:
      UI_Init( arg0 );
      return 0;

    case UI_SHUTDOWN:
      UI_Shutdown();
      return 0;

    case UI_KEY_EVENT:
      UI_KeyEvent( arg0, arg1 );
      return 0;

    case UI_MOUSE_EVENT:
      UI_MouseEvent( arg0, arg1 );
      return 0;

    case UI_MOUSE_POSITION:
      return UI_MousePosition( );

    case UI_SET_MOUSE_POSITION:
      UI_SetMousePosition( arg0, arg1 );
      return 0;

    case UI_REFRESH:
      UI_Refresh( arg0 );
      return 0;

    case UI_IS_FULLSCREEN:
      return UI_IsFullscreen( );

    case UI_SET_ACTIVE_MENU:
      UI_SetActiveMenu( arg0 );
      return 0;

    case UI_CONSOLE_COMMAND:
      return UI_ConsoleCommand( arg0 );

    case UI_DRAW_CONNECT_SCREEN:
      UI_DrawConnectScreen( arg0 );
      return 0;
  }

  return -1;
}



void AssetCache( void )
{
  int i;

  uiInfo.uiDC.Assets.gradientBar = trap_R_RegisterShaderNoMip( ASSET_GRADIENTBAR );
  uiInfo.uiDC.Assets.scrollBar = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR );
  uiInfo.uiDC.Assets.scrollBarArrowDown = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWDOWN );
  uiInfo.uiDC.Assets.scrollBarArrowUp = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWUP );
  uiInfo.uiDC.Assets.scrollBarArrowLeft = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWLEFT );
  uiInfo.uiDC.Assets.scrollBarArrowRight = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWRIGHT );
  uiInfo.uiDC.Assets.scrollBarThumb = trap_R_RegisterShaderNoMip( ASSET_SCROLL_THUMB );
  uiInfo.uiDC.Assets.sliderBar = trap_R_RegisterShaderNoMip( ASSET_SLIDER_BAR );
  uiInfo.uiDC.Assets.sliderThumb = trap_R_RegisterShaderNoMip( ASSET_SLIDER_THUMB );

  if( ui_emoticons.integer ) 
  {
    uiInfo.uiDC.Assets.emoticonCount = BG_LoadEmoticons(
      uiInfo.uiDC.Assets.emoticons, MAX_EMOTICONS );
  }
  else
    uiInfo.uiDC.Assets.emoticonCount = 0;

  for( i = 0; i < uiInfo.uiDC.Assets.emoticonCount; i++ )
  {
    uiInfo.uiDC.Assets.emoticons[ i ].shader = trap_R_RegisterShaderNoMip( 
      va( "emoticons/%s_%dx1.tga", uiInfo.uiDC.Assets.emoticons[ i ].name,
          uiInfo.uiDC.Assets.emoticons[ i ].width ) );
  }
}

void UI_DrawSides( float x, float y, float w, float h, float size )
{
  float sizeY;

  UI_AdjustFrom640( &x, &y, &w, &h );
  sizeY = size * uiInfo.uiDC.yscale;
  size *= uiInfo.uiDC.xscale;

  trap_R_DrawStretchPic( x, y + sizeY, size, h - ( sizeY * 2.0f ), 0, 0, 0, 0, uiInfo.uiDC.whiteShader );
  trap_R_DrawStretchPic( x + w - size, y + sizeY, size, h - ( sizeY * 2.0f ), 0, 0, 0, 0, uiInfo.uiDC.whiteShader );
}

void UI_DrawTopBottom( float x, float y, float w, float h, float size )
{
  UI_AdjustFrom640( &x, &y, &w, &h );
  size *= uiInfo.uiDC.yscale;
  trap_R_DrawStretchPic( x, y, w, size, 0, 0, 0, 0, uiInfo.uiDC.whiteShader );
  trap_R_DrawStretchPic( x, y + h - size, w, size, 0, 0, 0, 0, uiInfo.uiDC.whiteShader );
}

/*
================
UI_DrawRect
 
Coordinates are 640*480 virtual values
=================
*/
void UI_DrawRect( float x, float y, float width, float height, float size, const float *color )
{
  trap_R_SetColor( color );

  UI_DrawTopBottom( x, y, width, height, size );
  UI_DrawSides( x, y, width, height, size );

  trap_R_SetColor( NULL );
}

/*
==================
UI_ServerInfoIsValid

Return false if the infostring contains nonprinting characters, 
 or if the hostname is blank/undefined
==================
*/
static qboolean UI_ServerInfoIsValid( char *info )
{
  char *c;
  int  len = 0;

  for( c = info; *c; c++ )
  {
    if( !isprint( *c ) )
      return qfalse;
  }

  for( c = Info_ValueForKey( info, "hostname" ); *c; c++ )
  {
    if( isgraph( *c ) )
      len++;
  }

  if( len )
    return qtrue;
  else
    return qfalse;
}

/*
==================
UI_InsertServerIntoDisplayList
==================
*/
static void UI_InsertServerIntoDisplayList( int num, int position )
{
  int         i;
  static char info[MAX_STRING_CHARS];

  if( position < 0 || position > uiInfo.serverStatus.numDisplayServers )
    return;

  trap_LAN_GetServerInfo( ui_netSource.integer, num, info, MAX_STRING_CHARS );

  if( !UI_ServerInfoIsValid( info ) ) // don't list servers with invalid info
    return;

  uiInfo.serverStatus.numDisplayServers++;

  for( i = uiInfo.serverStatus.numDisplayServers; i > position; i-- )
    uiInfo.serverStatus.displayServers[i] = uiInfo.serverStatus.displayServers[i-1];

  uiInfo.serverStatus.displayServers[position] = num;
}

/*
==================
UI_RemoveServerFromDisplayList
==================
*/
static void UI_RemoveServerFromDisplayList( int num )
{
  int i, j;
  static char info[MAX_STRING_CHARS];

  for( i = 0; i < uiInfo.serverStatus.numDisplayServers; i++ )
  {
    if( uiInfo.serverStatus.displayServers[i] == num )
    {
      uiInfo.serverStatus.numDisplayServers--;

      trap_LAN_GetServerInfo( ui_netSource.integer, num, info, MAX_STRING_CHARS );

      for( j = i; j < uiInfo.serverStatus.numDisplayServers; j++ )
        uiInfo.serverStatus.displayServers[j] = uiInfo.serverStatus.displayServers[j+1];

      return;
    }
  }
}

/*
==================
UI_BinaryServerInsertion
==================
*/
static void UI_BinaryServerInsertion( int num )
{
  int mid, offset, res, len;

  // use binary search to insert server
  len = uiInfo.serverStatus.numDisplayServers;
  mid = len;
  offset = 0;
  res = 0;

  while( mid > 0 )
  {
    mid = len >> 1;
    //
    res = trap_LAN_CompareServers( ui_netSource.integer, uiInfo.serverStatus.sortKey,
                                   uiInfo.serverStatus.sortDir, num, uiInfo.serverStatus.displayServers[offset+mid] );
    // if equal

    if( res == 0 )
    {
      UI_InsertServerIntoDisplayList( num, offset + mid );
      return;
    }

    // if larger
    else if( res == 1 )
    {
      offset += mid;
      len -= mid;
    }

    // if smaller
    else
      len -= mid;
  }

  if( res == 1 )
    offset++;

  UI_InsertServerIntoDisplayList( num, offset );
}

typedef struct
{
  char *name, *altName;
}

serverStatusCvar_t;

serverStatusCvar_t serverStatusCvars[] = {
      {"sv_hostname", "Name"},
      {"Address", ""},
      {"gamename", "Game name"},
      {"mapname", "Map"},
      {"version", ""},
      {"protocol", ""},
      {"timelimit", ""},
      {NULL, NULL}
    };

/*
==================
UI_SortServerStatusInfo
==================
*/

static int UI_SortServerStatusCompare( const void *a, const void *b )
{
  const char **la = (const char **)a;
  const char **lb = (const char **)b;

  return strcmp( la[0], lb[0] );
}

static void UI_SortServerStatusInfo( serverStatusInfo_t *info )
{
  int i, j, index;
  char *tmp1, *tmp2;

  index = 0;

  for( i = 0; serverStatusCvars[i].name; i++ )
  {
    for( j = 0; j < info->numLines; j++ )
    {
      if( !info->lines[j][1] || info->lines[j][1][0] )
        continue;

      if( !Q_stricmp( serverStatusCvars[i].name, info->lines[j][0] ) )
      {
        // swap lines
        tmp1 = info->lines[index][0];
        tmp2 = info->lines[index][3];
        info->lines[index][0] = info->lines[j][0];
        info->lines[index][3] = info->lines[j][3];
        info->lines[j][0] = tmp1;
        info->lines[j][3] = tmp2;
        //

        if( strlen( serverStatusCvars[i].altName ) )
          info->lines[index][0] = serverStatusCvars[i].altName;

        index++;
      }
    }
  }

  // sort remaining cvars
  qsort( info->lines + index,
         info->numLines - index, sizeof( info->lines[ 0 ] ),
         UI_SortServerStatusCompare );
}

/*
==================
UI_GetServerStatusInfo
==================
*/
static int UI_GetServerStatusInfo( const char *serverAddress, serverStatusInfo_t *info )
{
  char *p, *score, *ping, *name;
  int i, len;

  if( !info )
  {
    trap_LAN_ServerStatus( serverAddress, NULL, 0 );
    return qfalse;
  }

  memset( info, 0, sizeof( *info ) );

  if( trap_LAN_ServerStatus( serverAddress, info->text, sizeof( info->text ) ) )
  {
    Q_strncpyz( info->address, serverAddress, sizeof( info->address ) );
    p = info->text;
    info->numLines = 0;
    info->lines[info->numLines][0] = "Address";
    info->lines[info->numLines][1] = "";
    info->lines[info->numLines][2] = "";
    info->lines[info->numLines][3] = info->address;
    info->numLines++;
    // get the cvars

    while( p && *p )
    {
      p = strchr( p, '\\' );

      if( !p ) break;

      *p++ = '\0';

      if( *p == '\\' )
        break;

      info->lines[info->numLines][0] = p;
      info->lines[info->numLines][1] = "";
      info->lines[info->numLines][2] = "";

      p = strchr( p, '\\' );

      if( !p ) break;

      *p++ = '\0';

      info->lines[info->numLines][3] = p;
      info->numLines++;

      if( info->numLines >= MAX_SERVERSTATUS_LINES )
        break;
    }

    UI_SortServerStatusInfo( info );

    // get the player list
    if( info->numLines < MAX_SERVERSTATUS_LINES - 3 )
    {
      // empty line
      info->lines[info->numLines][0] = "";
      info->lines[info->numLines][1] = "";
      info->lines[info->numLines][2] = "";
      info->lines[info->numLines][3] = "";
      info->numLines++;
      // header
      info->lines[info->numLines][0] = "num";
      info->lines[info->numLines][1] = "score";
      info->lines[info->numLines][2] = "ping";
      info->lines[info->numLines][3] = "name";
      info->numLines++;
      // parse players
      i = 0;
      len = 0;

      while( p && *p )
      {
        if( *p == '\\' )
          *p++ = '\0';

        if( !p )
          break;

        score = p;

        p = strchr( p, ' ' );

        if( !p )
          break;

        *p++ = '\0';

        ping = p;

        p = strchr( p, ' ' );

        if( !p )
          break;

        *p++ = '\0';

        name = p;

        Com_sprintf( &info->pings[len], sizeof( info->pings ) - len, "%d", i );

        info->lines[info->numLines][0] = &info->pings[len];

        len += strlen( &info->pings[len] ) + 1;

        info->lines[info->numLines][1] = score;
        info->lines[info->numLines][2] = ping;
        info->lines[info->numLines][3] = name;
        info->numLines++;

        if( info->numLines >= MAX_SERVERSTATUS_LINES )
          break;

        p = strchr( p, '\\' );

        if( !p )
          break;

        *p++ = '\0';

        //
        i++;
      }
    }

    return qtrue;
  }

  return qfalse;
}

/*
==================
stristr
==================
*/
static char *stristr( char *str, char *charset )
{
  int i;

  while( *str )
  {
    for( i = 0; charset[i] && str[i]; i++ )
      if( toupper( charset[i] ) != toupper( str[i] ) ) break;

    if( !charset[i] ) return str;

    str++;
  }

  return NULL;
}

/*
==================
UI_BuildFindPlayerList
==================
*/
static void UI_FeederSelection( int feederID, int index );

static void UI_BuildFindPlayerList( qboolean force )
{
  static int numFound, numTimeOuts;
  int i, j, k, resend;
  serverStatusInfo_t info;
  char name[MAX_NAME_LENGTH+2];
  char infoString[MAX_STRING_CHARS];
  qboolean duplicate;

  if( !force )
  {
    if( !uiInfo.nextFindPlayerRefresh || uiInfo.nextFindPlayerRefresh > uiInfo.uiDC.realTime )
      return;
  }
  else
  {
    memset( &uiInfo.pendingServerStatus, 0, sizeof( uiInfo.pendingServerStatus ) );
    uiInfo.numFoundPlayerServers = 0;
    uiInfo.currentFoundPlayerServer = 0;
    trap_Cvar_VariableStringBuffer( "ui_findPlayer", uiInfo.findPlayerName, sizeof( uiInfo.findPlayerName ) );
    Q_CleanStr( uiInfo.findPlayerName );
    // should have a string of some length

    if( !strlen( uiInfo.findPlayerName ) )
    {
      uiInfo.nextFindPlayerRefresh = 0;
      return;
    }

    // set resend time
    resend = ui_serverStatusTimeOut.integer / 2 - 10;

    if( resend < 50 )
      resend = 50;

    trap_Cvar_Set( "cl_serverStatusResendTime", va( "%d", resend ) );
    // reset all server status requests
    trap_LAN_ServerStatus( NULL, NULL, 0 );
    //
    uiInfo.numFoundPlayerServers = 1;
    Com_sprintf( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1],
                 sizeof( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1] ),
                 "searching %d...", uiInfo.pendingServerStatus.num );
    numFound = 0;
    numTimeOuts++;
  }

  for( i = 0; i < MAX_SERVERSTATUSREQUESTS; i++ )
  {
    // if this pending server is valid

    if( uiInfo.pendingServerStatus.server[i].valid )
    {
      // try to get the server status for this server

      if( UI_GetServerStatusInfo( uiInfo.pendingServerStatus.server[i].adrstr, &info ) )
      {
        //
        numFound++;
        // parse through the server status lines

        for( j = 0; j < info.numLines; j++ )
        {
          // should have ping info

          if( !info.lines[j][2] || !info.lines[j][2][0] )
            continue;

          // clean string first
          Q_strncpyz( name, info.lines[j][3], sizeof( name ) );

          Q_CleanStr( name );

          duplicate = qfalse;

          for( k = 0; k < uiInfo.numFoundPlayerServers - 1; k++ )
          {
            if( Q_strncmp( uiInfo.foundPlayerServerAddresses[ k ],
                           uiInfo.pendingServerStatus.server[ i ].adrstr,
                           MAX_ADDRESSLENGTH ) == 0 )
              duplicate = qtrue;
          }

          // if the player name is a substring
          if( stristr( name, uiInfo.findPlayerName ) && !duplicate )
          {
            // add to found server list if we have space (always leave space for a line with the number found)

            if( uiInfo.numFoundPlayerServers < MAX_FOUNDPLAYER_SERVERS - 1 )
            {
              //
              Q_strncpyz( uiInfo.foundPlayerServerAddresses[uiInfo.numFoundPlayerServers-1],
                          uiInfo.pendingServerStatus.server[i].adrstr,
                          sizeof( uiInfo.foundPlayerServerAddresses[0] ) );
              Q_strncpyz( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1],
                          uiInfo.pendingServerStatus.server[i].name,
                          sizeof( uiInfo.foundPlayerServerNames[0] ) );
              uiInfo.numFoundPlayerServers++;
            }
            else
            {
              // can't add any more so we're done
              uiInfo.pendingServerStatus.num = uiInfo.serverStatus.numDisplayServers;
            }
          }
        }

        Com_sprintf( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1],
                     sizeof( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1] ),
                     "searching %d/%d...", numFound, uiInfo.pendingServerStatus.num );
        // retrieved the server status so reuse this spot
        uiInfo.pendingServerStatus.server[i].valid = qfalse;
      }
    }

    // if empty pending slot or timed out
    if( !uiInfo.pendingServerStatus.server[i].valid ||
         uiInfo.pendingServerStatus.server[i].startTime < uiInfo.uiDC.realTime - ui_serverStatusTimeOut.integer )
    {
      if( uiInfo.pendingServerStatus.server[i].valid )
        numTimeOuts++;

      // reset server status request for this address
      UI_GetServerStatusInfo( uiInfo.pendingServerStatus.server[i].adrstr, NULL );

      // reuse pending slot
      uiInfo.pendingServerStatus.server[i].valid = qfalse;

      // if we didn't try to get the status of all servers in the main browser yet
      if( uiInfo.pendingServerStatus.num < uiInfo.serverStatus.numDisplayServers )
      {
        uiInfo.pendingServerStatus.server[i].startTime = uiInfo.uiDC.realTime;
        trap_LAN_GetServerAddressString( ui_netSource.integer,
                                         uiInfo.serverStatus.displayServers[uiInfo.pendingServerStatus.num],
                                         uiInfo.pendingServerStatus.server[i].adrstr,
                                         sizeof( uiInfo.pendingServerStatus.server[i].adrstr ) );

        trap_LAN_GetServerInfo( ui_netSource.integer,
                                uiInfo.serverStatus.displayServers[uiInfo.pendingServerStatus.num],
                                infoString, sizeof( infoString ) );

        Q_strncpyz( uiInfo.pendingServerStatus.server[i].name,
                    Info_ValueForKey( infoString, "hostname" ),
                    sizeof( uiInfo.pendingServerStatus.server[0].name ) );

        uiInfo.pendingServerStatus.server[i].valid = qtrue;
        uiInfo.pendingServerStatus.num++;
        Com_sprintf( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1],
                     sizeof( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1] ),
                     "searching %d/%d...", numFound, uiInfo.pendingServerStatus.num );
      }
    }
  }

  for( i = 0; i < MAX_SERVERSTATUSREQUESTS; i++ )
  {
    if( uiInfo.pendingServerStatus.server[i].valid )
      break;
  }

  // if still trying to retrieve server status info
  if( i < MAX_SERVERSTATUSREQUESTS )
    uiInfo.nextFindPlayerRefresh = uiInfo.uiDC.realTime + 25;
  else
  {
    // add a line that shows the number of servers found

    if( !uiInfo.numFoundPlayerServers )
    {
      Com_sprintf( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1],
                   sizeof( uiInfo.foundPlayerServerAddresses[0] ), "no servers found" );
    }
    else
    {
      Com_sprintf( uiInfo.foundPlayerServerNames[uiInfo.numFoundPlayerServers-1],
                   sizeof( uiInfo.foundPlayerServerAddresses[0] ),
                   "%d server%s found with player %s", uiInfo.numFoundPlayerServers - 1,
                   uiInfo.numFoundPlayerServers == 2 ? "" : "s", uiInfo.findPlayerName );
    }

    uiInfo.nextFindPlayerRefresh = 0;
    // show the server status info for the selected server
    UI_FeederSelection( FEEDER_FINDPLAYER, uiInfo.currentFoundPlayerServer );
  }
}

/*
==================
UI_BuildServerStatus
==================
*/
static void UI_BuildServerStatus( qboolean force )
{
  if( uiInfo.nextFindPlayerRefresh )
    return;

  if( !force )
  {
    if( !uiInfo.nextServerStatusRefresh || uiInfo.nextServerStatusRefresh > uiInfo.uiDC.realTime )
      return;
  }
  else
  {
    Menu_SetFeederSelection( NULL, FEEDER_SERVERSTATUS, 0, NULL );
    uiInfo.serverStatusInfo.numLines = 0;
    // reset all server status requests
    trap_LAN_ServerStatus( NULL, NULL, 0 );
  }

  if( uiInfo.serverStatus.currentServer < 0 ||
      uiInfo.serverStatus.currentServer > uiInfo.serverStatus.numDisplayServers ||
      uiInfo.serverStatus.numDisplayServers == 0 )
    return;

  if( UI_GetServerStatusInfo( uiInfo.serverStatusAddress, &uiInfo.serverStatusInfo ) )
  {
    uiInfo.nextServerStatusRefresh = 0;
    UI_GetServerStatusInfo( uiInfo.serverStatusAddress, NULL );
  }
  else
    uiInfo.nextServerStatusRefresh = uiInfo.uiDC.realTime + 500;
}

/*
==================
UI_BuildServerDisplayList
==================
*/
static void UI_BuildServerDisplayList( int force )
{
  int i, count, clients, maxClients, ping, len, visible;
  char info[MAX_STRING_CHARS];
  static int numinvisible;

  if( !( force || uiInfo.uiDC.realTime > uiInfo.serverStatus.nextDisplayRefresh ) )
    return;

  // if we shouldn't reset
  if( force == 2 )
    force = 0;

  // do motd updates here too
  trap_Cvar_VariableStringBuffer( "cl_motdString", uiInfo.serverStatus.motd, sizeof( uiInfo.serverStatus.motd ) );

  len = strlen( uiInfo.serverStatus.motd );

  if( len != uiInfo.serverStatus.motdLen )
  {
    uiInfo.serverStatus.motdLen = len;
    uiInfo.serverStatus.motdWidth = -1;
  }

  if( force )
  {
    numinvisible = 0;
    // clear number of displayed servers
    uiInfo.serverStatus.numDisplayServers = 0;
    uiInfo.serverStatus.numPlayersOnServers = 0;
    // set list box index to zero
    Menu_SetFeederSelection( NULL, FEEDER_SERVERS, 0, NULL );
    // mark all servers as visible so we store ping updates for them
    trap_LAN_MarkServerVisible( ui_netSource.integer, -1, qtrue );
  }

  // get the server count (comes from the master)
  count = trap_LAN_GetServerCount( ui_netSource.integer );

  if( count == -1 || ( ui_netSource.integer == AS_LOCAL && count == 0 ) )
  {
    // still waiting on a response from the master
    uiInfo.serverStatus.numDisplayServers = 0;
    uiInfo.serverStatus.numPlayersOnServers = 0;
    uiInfo.serverStatus.nextDisplayRefresh = uiInfo.uiDC.realTime + 500;
    return;
  }

  visible = qfalse;

  for( i = 0; i < count; i++ )
  {
    // if we already got info for this server

    if( !trap_LAN_ServerIsVisible( ui_netSource.integer, i ) )
      continue;

    visible = qtrue;
    // get the ping for this server
    ping = trap_LAN_GetServerPing( ui_netSource.integer, i );

    if( ping > 0 || ui_netSource.integer == AS_FAVORITES )
    {
      trap_LAN_GetServerInfo( ui_netSource.integer, i, info, MAX_STRING_CHARS );

      clients = atoi( Info_ValueForKey( info, "clients" ) );
      uiInfo.serverStatus.numPlayersOnServers += clients;

      if( ui_browserShowEmpty.integer == 0 )
      {
        if( clients == 0 )
        {
          trap_LAN_MarkServerVisible( ui_netSource.integer, i, qfalse );
          continue;
        }
      }

      if( ui_browserShowFull.integer == 0 )
      {
        maxClients = atoi( Info_ValueForKey( info, "sv_maxclients" ) );

        if( clients == maxClients )
        {
          trap_LAN_MarkServerVisible( ui_netSource.integer, i, qfalse );
          continue;
        }
      }

      // make sure we never add a favorite server twice
      if( ui_netSource.integer == AS_FAVORITES )
        UI_RemoveServerFromDisplayList( i );

      // insert the server into the list
      UI_BinaryServerInsertion( i );

      // done with this server
      if( ping > 0 )
      {
        trap_LAN_MarkServerVisible( ui_netSource.integer, i, qfalse );
        numinvisible++;
      }
    }
  }

  uiInfo.serverStatus.refreshtime = uiInfo.uiDC.realTime;

  // if there were no servers visible for ping updates

  if( !visible )
  {
//    UI_StopServerRefresh();
//    uiInfo.serverStatus.nextDisplayRefresh = 0;
  }
}


/*
=================
UI_StopServerRefresh
=================
*/
static void UI_StopServerRefresh( void )
{
  int count;

  if( !uiInfo.serverStatus.refreshActive )
  {
    // not currently refreshing
    return;
  }

  uiInfo.serverStatus.refreshActive = qfalse;
  Com_Printf( "%d servers listed in browser with %d players.\n",
              uiInfo.serverStatus.numDisplayServers,
              uiInfo.serverStatus.numPlayersOnServers );
  count = trap_LAN_GetServerCount( ui_netSource.integer );

  if( count - uiInfo.serverStatus.numDisplayServers > 0 )
  {
    Com_Printf( "%d servers not listed due to packet loss, invalid info,"
                " or pings higher than %d\n",
                count - uiInfo.serverStatus.numDisplayServers,
                ( int ) trap_Cvar_VariableValue( "cl_maxPing" ) );
  }

}

/*
=================
UI_DoServerRefresh
=================
*/
static void UI_DoServerRefresh( void )
{
  qboolean wait = qfalse;

  if( !uiInfo.serverStatus.refreshActive )
    return;

  if( ui_netSource.integer != AS_FAVORITES )
  {
    if( ui_netSource.integer == AS_LOCAL )
    {
      if( !trap_LAN_GetServerCount( ui_netSource.integer ) )
        wait = qtrue;
    }
    else
    {
      if( trap_LAN_GetServerCount( ui_netSource.integer ) < 0 )
        wait = qtrue;
    }
  }

  if( uiInfo.uiDC.realTime < uiInfo.serverStatus.refreshtime )
  {
    if( wait )
      return;
  }

  // if still trying to retrieve pings
  if( trap_LAN_UpdateVisiblePings( ui_netSource.integer ) )
    uiInfo.serverStatus.refreshtime = uiInfo.uiDC.realTime + 1000;
  else if( !wait )
  {
    // get the last servers in the list
    UI_BuildServerDisplayList( 2 );
    // stop the refresh
    UI_StopServerRefresh();
  }

  //
  UI_BuildServerDisplayList( qfalse );
}

/*
=================
UI_UpdatePendingPings
=================
*/
static void UI_UpdatePendingPings( void )
{
  trap_LAN_ResetPings( ui_netSource.integer );
  uiInfo.serverStatus.refreshActive = qtrue;
  uiInfo.serverStatus.refreshtime = uiInfo.uiDC.realTime + 1000;

}

/*
=================
UI_StartServerRefresh
=================
*/
static void UI_StartServerRefresh( qboolean full )
{
  int   time;
  qtime_t q;

  time = trap_RealTime( &q );
  trap_Cvar_Set( va( "ui_lastServerRefresh_%i_time", ui_netSource.integer ),
                 va( "%i", time ) );
  trap_Cvar_Set( va( "ui_lastServerRefresh_%i", ui_netSource.integer ),
                 va( "%s-%i, %i at %i:%02i", MonthAbbrev[q.tm_mon],
                     q.tm_mday, 1900 + q.tm_year, q.tm_hour, q.tm_min ) );

  if( !full )
  {
    UI_UpdatePendingPings();
    return;
  }

  uiInfo.serverStatus.refreshActive = qtrue;
  uiInfo.serverStatus.nextDisplayRefresh = uiInfo.uiDC.realTime + 1000;
  // clear number of displayed servers
  uiInfo.serverStatus.numDisplayServers = 0;
  uiInfo.serverStatus.numPlayersOnServers = 0;
  // mark all servers as visible so we store ping updates for them
  trap_LAN_MarkServerVisible( ui_netSource.integer, -1, qtrue );
  // reset all the pings
  trap_LAN_ResetPings( ui_netSource.integer );
  //

  if( ui_netSource.integer == AS_LOCAL )
  {
    trap_Cmd_ExecuteText( EXEC_APPEND, "localservers\n" );
    uiInfo.serverStatus.refreshtime = uiInfo.uiDC.realTime + 1000;
    return;
  }

  uiInfo.serverStatus.refreshtime = uiInfo.uiDC.realTime + 5000;

  if( ui_netSource.integer == AS_GLOBAL || ui_netSource.integer == AS_MPLAYER )
  {
    qboolean global = ui_netSource.integer == AS_GLOBAL;

    trap_Cmd_ExecuteText( EXEC_APPEND, va( "globalservers %d 70 full empty\n",
                          global ? 0 : 1 ) );
  }
}

int frameCount = 0;
int startTime;

#define UI_FPS_FRAMES 4
void UI_Refresh( int realtime )
{
  static int index;
  static int  previousTimes[UI_FPS_FRAMES];

  //if( !( trap_Key_GetCatcher() & KEYCATCH_UI ) ) {
  //  return;
  //}

  uiInfo.uiDC.frameTime = realtime - uiInfo.uiDC.realTime;
  uiInfo.uiDC.realTime = realtime;

  previousTimes[index % UI_FPS_FRAMES] = uiInfo.uiDC.frameTime;
  index++;

  if( index > UI_FPS_FRAMES )
  {
    int i, total;
    // average multiple frames together to smooth changes out a bit
    total = 0;

    for( i = 0 ; i < UI_FPS_FRAMES ; i++ )
      total += previousTimes[i];

    if( !total )
      total = 1;

    uiInfo.uiDC.FPS = 1000 * UI_FPS_FRAMES / total;
  }

  UI_UpdateCvars();

  if( Menu_Count() > 0 )
  {
    Menu_UpdateAll( );
    Menu_PaintAll( );
    UI_DoServerRefresh( );
    UI_BuildServerStatus( qfalse );
    UI_BuildFindPlayerList( qfalse );
    UI_UpdateNews( qfalse );
  }

  // draw cursor
  UI_SetColor( NULL );

  if( trap_Key_GetCatcher( ) == KEYCATCH_UI && !trap_Cvar_VariableValue( "ui_hideCursor" ) )
  {
    UI_DrawHandlePic( uiInfo.uiDC.cursorx - ( 16.0f * uiInfo.uiDC.aspectScale ), uiInfo.uiDC.cursory - 16.0f,
                      32.0f * uiInfo.uiDC.aspectScale, 32.0f, uiInfo.uiDC.Assets.cursor );
  }
}

/*
=================
UI_Shutdown
=================
*/
void UI_Shutdown( void )
{
  trap_LAN_SaveCachedServers();
}

qboolean Asset_Parse( int handle )
{
  pc_token_t token;
  const char *tempStr;

  if( !trap_Parse_ReadToken( handle, &token ) )
    return qfalse;

  if( Q_stricmp( token.string, "{" ) != 0 )
    return qfalse;

  while( 1 )
  {
    memset( &token, 0, sizeof( pc_token_t ) );

    if( !trap_Parse_ReadToken( handle, &token ) )
      return qfalse;

    if( Q_stricmp( token.string, "}" ) == 0 )
      return qtrue;

    // font
    if( Q_stricmp( token.string, "font" ) == 0 )
    {
      int pointSize;

      if( !PC_String_Parse( handle, &tempStr ) || !PC_Int_Parse( handle, &pointSize ) )
        return qfalse;

      trap_R_RegisterFont( tempStr, pointSize, &uiInfo.uiDC.Assets.textFont );
      uiInfo.uiDC.Assets.fontRegistered = qtrue;
      continue;
    }

    if( Q_stricmp( token.string, "smallFont" ) == 0 )
    {
      int pointSize;

      if( !PC_String_Parse( handle, &tempStr ) || !PC_Int_Parse( handle, &pointSize ) )
        return qfalse;

      trap_R_RegisterFont( tempStr, pointSize, &uiInfo.uiDC.Assets.smallFont );
      continue;
    }

    if( Q_stricmp( token.string, "bigFont" ) == 0 )
    {
      int pointSize;

      if( !PC_String_Parse( handle, &tempStr ) || !PC_Int_Parse( handle, &pointSize ) )
        return qfalse;

      trap_R_RegisterFont( tempStr, pointSize, &uiInfo.uiDC.Assets.bigFont );
      continue;
    }


    // gradientbar
    if( Q_stricmp( token.string, "gradientbar" ) == 0 )
    {
      if( !PC_String_Parse( handle, &tempStr ) )
        return qfalse;

      uiInfo.uiDC.Assets.gradientBar = trap_R_RegisterShaderNoMip( tempStr );
      continue;
    }

    // enterMenuSound
    if( Q_stricmp( token.string, "menuEnterSound" ) == 0 )
    {
      if( !PC_String_Parse( handle, &tempStr ) )
        return qfalse;

      uiInfo.uiDC.Assets.menuEnterSound = trap_S_RegisterSound( tempStr, qfalse );
      continue;
    }

    // exitMenuSound
    if( Q_stricmp( token.string, "menuExitSound" ) == 0 )
    {
      if( !PC_String_Parse( handle, &tempStr ) )
        return qfalse;

      uiInfo.uiDC.Assets.menuExitSound = trap_S_RegisterSound( tempStr, qfalse );
      continue;
    }

    // itemFocusSound
    if( Q_stricmp( token.string, "itemFocusSound" ) == 0 )
    {
      if( !PC_String_Parse( handle, &tempStr ) )
        return qfalse;

      uiInfo.uiDC.Assets.itemFocusSound = trap_S_RegisterSound( tempStr, qfalse );
      continue;
    }

    // menuBuzzSound
    if( Q_stricmp( token.string, "menuBuzzSound" ) == 0 )
    {
      if( !PC_String_Parse( handle, &tempStr ) )
        return qfalse;

      uiInfo.uiDC.Assets.menuBuzzSound = trap_S_RegisterSound( tempStr, qfalse );
      continue;
    }

    if( Q_stricmp( token.string, "cursor" ) == 0 )
    {
      if( !PC_String_Parse( handle, &uiInfo.uiDC.Assets.cursorStr ) )
        return qfalse;

      uiInfo.uiDC.Assets.cursor = trap_R_RegisterShaderNoMip( uiInfo.uiDC.Assets.cursorStr );
      continue;
    }

    if( Q_stricmp( token.string, "fadeClamp" ) == 0 )
    {
      if( !PC_Float_Parse( handle, &uiInfo.uiDC.Assets.fadeClamp ) )
        return qfalse;

      continue;
    }

    if( Q_stricmp( token.string, "fadeCycle" ) == 0 )
    {
      if( !PC_Int_Parse( handle, &uiInfo.uiDC.Assets.fadeCycle ) )
        return qfalse;

      continue;
    }

    if( Q_stricmp( token.string, "fadeAmount" ) == 0 )
    {
      if( !PC_Float_Parse( handle, &uiInfo.uiDC.Assets.fadeAmount ) )
        return qfalse;

      continue;
    }

    if( Q_stricmp( token.string, "shadowX" ) == 0 )
    {
      if( !PC_Float_Parse( handle, &uiInfo.uiDC.Assets.shadowX ) )
        return qfalse;

      continue;
    }

    if( Q_stricmp( token.string, "shadowY" ) == 0 )
    {
      if( !PC_Float_Parse( handle, &uiInfo.uiDC.Assets.shadowY ) )
        return qfalse;

      continue;
    }

    if( Q_stricmp( token.string, "shadowColor" ) == 0 )
    {
      if( !PC_Color_Parse( handle, &uiInfo.uiDC.Assets.shadowColor ) )
        return qfalse;

      uiInfo.uiDC.Assets.shadowFadeClamp = uiInfo.uiDC.Assets.shadowColor[3];
      continue;
    }

  }

  return qfalse;
}

void UI_Report( void )
{
  String_Report();
}

void UI_ParseMenu( const char *menuFile )
{
  int handle;
  pc_token_t token;

  handle = trap_Parse_LoadSource( menuFile );

  if( !handle )
  {
    Com_Printf( S_COLOR_YELLOW "WARNING: Menu file %s not found\n",
                menuFile );
    return;
  }

  while( 1 )
  {
    memset( &token, 0, sizeof( pc_token_t ) );

    if( !trap_Parse_ReadToken( handle, &token ) )
      break;

    //if( Q_stricmp( token, "{" ) ) {
    //  Com_Printf( "Missing { in menu file\n" );
    //  break;
    //}

    //if( menuCount == MAX_MENUS ) {
    //  Com_Printf( "Too many menus!\n" );
    //  break;
    //}

    if( token.string[0] == '}' )
      break;

    if( Q_stricmp( token.string, "assetGlobalDef" ) == 0 )
    {
      if( Asset_Parse( handle ) )
        continue;
      else
        break;
    }

    if( Q_stricmp( token.string, "menudef" ) == 0 )
    {
      // start a new menu
      Menu_New( handle );
    }
  }

  trap_Parse_FreeSource( handle );
}

qboolean Load_Menu( int handle )
{
  pc_token_t token;

  if( !trap_Parse_ReadToken( handle, &token ) )
    return qfalse;

  if( token.string[0] != '{' )
    return qfalse;

  while( 1 )
  {
    if( !trap_Parse_ReadToken( handle, &token ) )
      return qfalse;

    if( token.string[0] == 0 )
      return qfalse;

    if( token.string[0] == '}' )
      return qtrue;

    UI_ParseMenu( token.string );
  }

  return qfalse;
}

void UI_LoadMenus( const char *menuFile, qboolean reset )
{
  pc_token_t token;
  int handle;
  int start;

  start = trap_Milliseconds();

  handle = trap_Parse_LoadSource( menuFile );

  if( !handle )
    trap_Error( va( S_COLOR_RED "menu list '%s' not found, unable to continue!\n", menuFile ) );

  if( reset )
    Menu_Reset();

  while( 1 )
  {
    if( !trap_Parse_ReadToken( handle, &token ) )
      break;

    if( token.string[0] == 0 || token.string[0] == '}' )
      break;

    if( token.string[0] == '}' )
      break;

    if( Q_stricmp( token.string, "loadmenu" ) == 0 )
    {
      if( Load_Menu( handle ) )
        continue;
      else
        break;
    }
  }

  Com_Printf( "UI menu file '%s' loaded in %d msec\n", menuFile, trap_Milliseconds() - start );

  trap_Parse_FreeSource( handle );
}

void UI_LoadHelp( const char *helpFile )
{
  pc_token_t token;
  int handle, start;
  char title[ 32 ], buffer[ 1024 ];

  start = trap_Milliseconds();
  
  handle = trap_Parse_LoadSource( helpFile );
  if( !handle )
  {
    Com_Printf( S_COLOR_YELLOW "WARNING: help file '%s' not found!\n",
                helpFile );
    return;
  }

  if( !trap_Parse_ReadToken( handle, &token ) ||
      token.string[0] == 0 || token.string[0] != '{' )
  {
    Com_Printf( S_COLOR_YELLOW "WARNING: help file '%s' does not start with "
                "'{'\n", helpFile );
    return;
  }

  uiInfo.helpCount = 0;
  title[ 0 ] = 0;
  while( 1 )
  {
    if( !trap_Parse_ReadToken( handle, &token ) ||
        token.string[0] == 0 || token.string[0] == '}' )
      break;
      
    if( token.string[0] == '{' )
    {
      buffer[ 0 ] = 0;
      Q_strcat( buffer, sizeof( buffer ), title );
      Q_strcat( buffer, sizeof( buffer ), "\n\n" );
      while( trap_Parse_ReadToken( handle, &token ) &&
             token.string[0] != 0 && token.string[0] != '}' )
      {
        Q_strcat( buffer, sizeof( buffer ), token.string );
      }

      uiInfo.helpList[ uiInfo.helpCount ].text = String_Alloc( title );
      uiInfo.helpList[ uiInfo.helpCount ].v.text = String_Alloc( buffer );
      uiInfo.helpList[ uiInfo.helpCount ].type = INFOTYPE_TEXT;
      uiInfo.helpCount++;
      title[ 0 ] = 0;
    }
    else
      Q_strcat( title, sizeof( title ), token.string );
  }

  trap_Parse_FreeSource( handle );

  Com_Printf( "UI help file '%s' loaded in %d msec (%d infopanes)\n",
              helpFile, trap_Milliseconds() - start, uiInfo.helpCount );                   
}

void UI_Load( void )
{
  char lastName[1024];
  menuDef_t *menu = Menu_GetFocused();

  if( menu && menu->window.name )
    strcpy( lastName, menu->window.name );

  String_Init();

  UI_LoadMenus( "ui/menus.txt", qtrue );
  UI_LoadMenus( "ui/ingame.txt", qfalse );
  UI_LoadMenus( "ui/tremulous.txt", qfalse );
  UI_LoadHelp( "ui/help.txt" );
  Menus_CloseAll( );
  Menus_ActivateByName( lastName );

}

/*
===============
UI_GetCurrentAlienStage
===============
*/
static stage_t UI_GetCurrentAlienStage( void )
{
  char    buffer[ MAX_TOKEN_CHARS ];
  stage_t stage, dummy;

  trap_Cvar_VariableStringBuffer( "ui_stages", buffer, sizeof( buffer ) );
  sscanf( buffer, "%d %d", ( int * ) & stage , ( int * ) & dummy );

  return stage;
}

/*
===============
UI_GetCurrentHumanStage
===============
*/
static stage_t UI_GetCurrentHumanStage( void )
{
  char    buffer[ MAX_TOKEN_CHARS ];
  stage_t stage, dummy;

  trap_Cvar_VariableStringBuffer( "ui_stages", buffer, sizeof( buffer ) );
  sscanf( buffer, "%d %d", ( int * ) & dummy, ( int * ) & stage );

  return stage;
}

/*
===============
UI_DrawInfoPane
===============
*/
static void UI_DrawInfoPane( menuItem_t *item, rectDef_t *rect, float text_x, float text_y,
                             float scale, int textalign, int textvalign, vec4_t color, int textStyle )
{
  int         value = 0;
  const char  *s = "";
  char        *string = "";

  int         class, credits;
  char        ui_currentClass[ MAX_STRING_CHARS ];

  trap_Cvar_VariableStringBuffer( "ui_currentClass", ui_currentClass, MAX_STRING_CHARS );

  sscanf( ui_currentClass, "%d %d", &class, &credits );

  switch( item->type )
  {
    case INFOTYPE_TEXT:
      s = item->v.text;
      break;

    case INFOTYPE_CLASS:
      value = ( BG_ClassCanEvolveFromTo( class, item->v.pclass, credits,
                                         UI_GetCurrentAlienStage(), 0 ) +
                ALIEN_CREDITS_PER_KILL - 1 ) / ALIEN_CREDITS_PER_KILL;

      if( value < 1 )
      {
        s = va( "%s\n\n%s",
                BG_ClassConfig( item->v.pclass )->humanName,
                BG_Class( item->v.pclass )->info );
      }
      else
      {
        s = va( "%s\n\n%s\n\nFrags: %d",
                BG_ClassConfig( item->v.pclass )->humanName,
                BG_Class( item->v.pclass )->info,
                value );
      }

      break;

    case INFOTYPE_WEAPON:
      value = BG_Weapon( item->v.weapon )->price;

      if( value == 0 )
      {
        s = va( "%s\n\n%s\n\nCredits: Free",
                BG_Weapon( item->v.weapon )->humanName,
                BG_Weapon( item->v.weapon )->info );
      }
      else
      {
        s = va( "%s\n\n%s\n\nCredits: %d",
                BG_Weapon( item->v.weapon )->humanName,
                BG_Weapon( item->v.weapon )->info,
                value );
      }

      break;

    case INFOTYPE_UPGRADE:
      value = BG_Upgrade( item->v.upgrade )->price;

      if( value == 0 )
      {
        s = va( "%s\n\n%s\n\nCredits: Free",
                BG_Upgrade( item->v.upgrade )->humanName,
                BG_Upgrade( item->v.upgrade )->info );
      }
      else
      {
        s = va( "%s\n\n%s\n\nCredits: %d",
                BG_Upgrade( item->v.upgrade )->humanName,
                BG_Upgrade( item->v.upgrade )->info,
                value );
      }

      break;

    case INFOTYPE_BUILDABLE:
      value = BG_Buildable( item->v.buildable, NULL )->buildPoints;

      switch( BG_Buildable( item->v.buildable, NULL )->team )
      {
        case TEAM_ALIENS:
          string = "Sentience";
          break;

        case TEAM_HUMANS:
          string = "Mass";
          break;

        default:
          break;
      }

      if( value == 0 )
      {
        s = va( "%s\n\n%s",
                BG_Buildable( item->v.buildable, NULL )->humanName,
                BG_Buildable( item->v.buildable, NULL )->info );
      }
      else
      {
        s = va( "%s\n\n%s\n\n%s: %d",
                BG_Buildable( item->v.buildable, NULL )->humanName,
                BG_Buildable( item->v.buildable, NULL )->info,
                string, value );
      }

      break;
  }

  UI_DrawTextBlock( rect, text_x, text_y, color, scale,
                    textalign, textvalign, textStyle, s );
}


static void UI_DrawServerMapPreview( rectDef_t *rect, float scale, vec4_t color )
{
  if( uiInfo.serverStatus.currentServerCinematic >= 0 )
  {
    trap_CIN_RunCinematic( uiInfo.serverStatus.currentServerCinematic );
    trap_CIN_SetExtents( uiInfo.serverStatus.currentServerCinematic, rect->x, rect->y, rect->w, rect->h );
    trap_CIN_DrawCinematic( uiInfo.serverStatus.currentServerCinematic );
  }
  else if( uiInfo.serverStatus.currentServerPreview > 0 )
    UI_DrawHandlePic( rect->x, rect->y, rect->w, rect->h, uiInfo.serverStatus.currentServerPreview );
  else
    UI_DrawHandlePic( rect->x, rect->y, rect->w, rect->h, trap_R_RegisterShaderNoMip( "gfx/2d/load_screen" ) );
}

static void UI_DrawSelectedMapPreview( rectDef_t *rect, float scale, vec4_t color )
{
  int map = ui_selectedMap.integer;

  if( map < 0 || map > uiInfo.mapCount )
  {
    ui_selectedMap.integer = 0;
    trap_Cvar_Set( "ui_selectedMap", "0" );
    map = 0;
  }

  if( uiInfo.mapList[map].cinematic >= -1 )
  {
    if( uiInfo.mapList[map].cinematic == -1 )
      uiInfo.mapList[map].cinematic = trap_CIN_PlayCinematic( va( "%s.roq", uiInfo.mapList[map].mapLoadName ),
                                                              0, 0, 0, 0, ( CIN_loop | CIN_silent ) );

    if( uiInfo.mapList[map].cinematic >= 0 )
    {
      trap_CIN_RunCinematic( uiInfo.mapList[map].cinematic );
      trap_CIN_SetExtents( uiInfo.mapList[map].cinematic, rect->x, rect->y, rect->w, rect->h );
      trap_CIN_DrawCinematic( uiInfo.mapList[map].cinematic );
    }
    else
      uiInfo.mapList[map].cinematic = -2;
  }
  else
  {
    if( uiInfo.mapList[map].levelShot == -1 )
      uiInfo.mapList[map].levelShot = trap_R_RegisterShaderNoMip( uiInfo.mapList[map].imageName );

    if( uiInfo.mapList[map].levelShot > 0 )
      UI_DrawHandlePic( rect->x, rect->y, rect->w, rect->h, uiInfo.mapList[map].levelShot );
    else
      UI_DrawHandlePic( rect->x, rect->y, rect->w, rect->h, trap_R_RegisterShaderNoMip( "gfx/2d/load_screen" ) );
  }
}

static void UI_DrawSelectedMapName( rectDef_t *rect, float scale, vec4_t color, int textStyle )
{
  int map = ui_selectedMap.integer;

  if( map >= 0 && map < uiInfo.mapCount )
    UI_Text_Paint( rect->x, rect->y, scale, color, uiInfo.mapList[map].mapName, 0, 0, textStyle );
}

static const char *UI_OwnerDrawText( int ownerDraw )
{
  const char *s = NULL;

  switch( ownerDraw )
  {
    case UI_NETSOURCE:
      if( ui_netSource.integer < 0 || ui_netSource.integer >= numNetSources )
        ui_netSource.integer = 0;

      s = netSources[ui_netSource.integer];
      break;

    case UI_KEYBINDSTATUS:
      if( Display_KeyBindPending() )
        s = "Waiting for new key... Press ESCAPE to cancel";
      else
        s = "Press ENTER or CLICK to change, Press BACKSPACE to clear";

      break;

    case UI_SERVERREFRESHDATE:
      if( uiInfo.serverStatus.refreshActive )
      {
#define MAX_DOTS 5
        int numServers = trap_LAN_GetServerCount( ui_netSource.integer );
        int numDots = ( uiInfo.uiDC.realTime / 500 ) % ( MAX_DOTS + 1 );
        char dots[ MAX_DOTS + 1 ];
        int i;

        for( i = 0; i < numDots; i++ )
          dots[ i ] = '.';

        dots[ i ] = '\0';

        s = numServers < 0 ? va( "Waiting for response%s", dots ) :
            va( "Getting info for %d servers (ESC to cancel)%s", numServers, dots );
      }
      else
        s = va( "Refresh Time: %s", UI_Cvar_VariableString( va( "ui_lastServerRefresh_%i", ui_netSource.integer ) ) );

      break;

    case UI_SERVERMOTD:
      s = uiInfo.serverStatus.motd;
      break;

    default:
      break;
  }

  return s;
}

static int UI_OwnerDrawWidth( int ownerDraw, float scale )
{
  const char *s = NULL;

  switch( ownerDraw )
  {
    case UI_NETSOURCE:
    case UI_KEYBINDSTATUS:
    case UI_SERVERREFRESHDATE:
    case UI_SERVERMOTD:
      s = UI_OwnerDrawText( ownerDraw );
      break;

    default:
      break;
  }

  if( s )
    return UI_Text_Width( s, scale );

  return 0;
}

/*
===============
UI_BuildPlayerList
===============
*/
static void UI_BuildPlayerList( void )
{
  uiClientState_t cs;
  int   n, count, team, team2, playerTeamNumber;
  char  info[MAX_INFO_STRING];

  trap_GetClientState( &cs );
  trap_GetConfigString( CS_PLAYERS + cs.clientNum, info, MAX_INFO_STRING );
  uiInfo.playerNumber = cs.clientNum;
  team = atoi( Info_ValueForKey( info, "t" ) );
  trap_GetConfigString( CS_SERVERINFO, info, sizeof( info ) );
  count = atoi( Info_ValueForKey( info, "sv_maxclients" ) );
  uiInfo.playerCount = 0;
  uiInfo.myTeamCount = 0;
  uiInfo.myPlayerIndex = 0;
  playerTeamNumber = 0;

  for( n = 0; n < count; n++ )
  {
    trap_GetConfigString( CS_PLAYERS + n, info, MAX_INFO_STRING );

    if( info[0] )
    {
      Com_ClientListParse( &uiInfo.ignoreList[ uiInfo.playerCount ],
                          Info_ValueForKey( info, "ig" ) );
      Q_strncpyz( uiInfo.rawPlayerNames[uiInfo.playerCount],
                  Info_ValueForKey( info, "n" ), MAX_NAME_LENGTH );
      Q_strncpyz( uiInfo.playerNames[uiInfo.playerCount],
                  Info_ValueForKey( info, "n" ), MAX_NAME_LENGTH );
      Q_CleanStr( uiInfo.playerNames[uiInfo.playerCount] );
      uiInfo.clientNums[uiInfo.playerCount] = n;

      if( n == uiInfo.playerNumber )
        uiInfo.myPlayerIndex = uiInfo.playerCount;

      uiInfo.playerCount++;

      team2 = atoi( Info_ValueForKey( info, "t" ) );

      if( team2 == team )
      {
        Q_strncpyz( uiInfo.rawTeamNames[uiInfo.myTeamCount],
                    Info_ValueForKey( info, "n" ), MAX_NAME_LENGTH );
        Q_strncpyz( uiInfo.teamNames[uiInfo.myTeamCount],
                    Info_ValueForKey( info, "n" ), MAX_NAME_LENGTH );
        Q_CleanStr( uiInfo.teamNames[uiInfo.myTeamCount] );
        uiInfo.teamClientNums[uiInfo.myTeamCount] = n;

        if( uiInfo.playerNumber == n )
          playerTeamNumber = uiInfo.myTeamCount;

        uiInfo.myTeamCount++;
      }
    }
  }
}

static void UI_DrawGLInfo( rectDef_t *rect, float scale, int textalign, int textvalign,
                           vec4_t color, int textStyle, float text_x, float text_y )
{
  char      buffer[ 4096 ];

  Com_sprintf( buffer, sizeof( buffer ), "VENDOR: %s\nVERSION: %s\n"
               "PIXELFORMAT: color(%d-bits) Z(%d-bits) stencil(%d-bits)\n%s",
               uiInfo.uiDC.glconfig.vendor_string, uiInfo.uiDC.glconfig.renderer_string,
               uiInfo.uiDC.glconfig.colorBits, uiInfo.uiDC.glconfig.depthBits,
               uiInfo.uiDC.glconfig.stencilBits, uiInfo.uiDC.glconfig.extensions_string );

  UI_DrawTextBlock( rect, text_x, text_y, color, scale,
                    textalign, textvalign, textStyle, buffer );
}

// FIXME: table drive
//
static void UI_OwnerDraw( float x, float y, float w, float h,
                          float text_x, float text_y, int ownerDraw,
                          int ownerDrawFlags, int align,
                          int textalign, int textvalign, float borderSize,
                          float scale, vec4_t foreColor, vec4_t backColor,
                          qhandle_t shader,
                          int textStyle )
{
  rectDef_t       rect;

  rect.x = x;
  rect.y = y;
  rect.w = w;
  rect.h = h;

  switch( ownerDraw )
  {
    case UI_TEAMINFOPANE:
      UI_DrawInfoPane( &uiInfo.teamList[ uiInfo.teamIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_ACLASSINFOPANE:
      UI_DrawInfoPane( &uiInfo.alienClassList[ uiInfo.alienClassIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_AUPGRADEINFOPANE:
      UI_DrawInfoPane( &uiInfo.alienUpgradeList[ uiInfo.alienUpgradeIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_HITEMINFOPANE:
      UI_DrawInfoPane( &uiInfo.humanItemList[ uiInfo.humanItemIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_HBUYINFOPANE:
      UI_DrawInfoPane( &uiInfo.humanArmouryBuyList[ uiInfo.humanArmouryBuyIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_HSELLINFOPANE:
      UI_DrawInfoPane( &uiInfo.humanArmourySellList[ uiInfo.humanArmourySellIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_ABUILDINFOPANE:
      UI_DrawInfoPane( &uiInfo.alienBuildList[ uiInfo.alienBuildIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_HBUILDINFOPANE:
      UI_DrawInfoPane( &uiInfo.humanBuildList[ uiInfo.humanBuildIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_HELPINFOPANE:
      UI_DrawInfoPane( &uiInfo.helpList[ uiInfo.helpIndex ],
                       &rect, text_x, text_y, scale, textalign, textvalign, foreColor, textStyle );
      break;

    case UI_NETMAPPREVIEW:
      UI_DrawServerMapPreview( &rect, scale, foreColor );
      break;

    case UI_SELECTEDMAPPREVIEW:
      UI_DrawSelectedMapPreview( &rect, scale, foreColor );
      break;

    case UI_SELECTEDMAPNAME:
      UI_DrawSelectedMapName( &rect, scale, foreColor, textStyle );
      break;

    case UI_GLINFO:
      UI_DrawGLInfo( &rect, scale, textalign, textvalign, foreColor, textStyle, text_x, text_y );
      break;

    default:
      break;
  }

}

static qboolean UI_OwnerDrawVisible( int flags )
{
  qboolean vis = qtrue;
  uiClientState_t cs;
  team_t          team;
  char            info[ MAX_INFO_STRING ];

  trap_GetClientState( &cs );
  trap_GetConfigString( CS_PLAYERS + cs.clientNum, info, MAX_INFO_STRING );
  team = atoi( Info_ValueForKey( info, "t" ) );


  while( flags )
  {
    if( flags & UI_SHOW_NOTSPECTATING )
    {
      if( team == TEAM_NONE )
        vis = qfalse;

      flags &= ~UI_SHOW_NOTSPECTATING;
    }

    if( flags & UI_SHOW_VOTEACTIVE )
    {
      if( !trap_Cvar_VariableValue( "ui_voteActive" ) )
        vis = qfalse;

      flags &= ~UI_SHOW_VOTEACTIVE;
    }

    if( flags & UI_SHOW_CANVOTE )
    {
      if( trap_Cvar_VariableValue( "ui_voteActive" ) )
        vis = qfalse;

      flags &= ~UI_SHOW_CANVOTE;
    }

    if( flags & UI_SHOW_TEAMVOTEACTIVE )
    {
      if( team == TEAM_ALIENS )
      {
        if( !trap_Cvar_VariableValue( "ui_alienTeamVoteActive" ) )
          vis = qfalse;
      }
      else if( team == TEAM_HUMANS )
      {
        if( !trap_Cvar_VariableValue( "ui_humanTeamVoteActive" ) )
          vis = qfalse;
      }

      flags &= ~UI_SHOW_TEAMVOTEACTIVE;
    }

    if( flags & UI_SHOW_CANTEAMVOTE )
    {
      if( team == TEAM_ALIENS )
      {
        if( trap_Cvar_VariableValue( "ui_alienTeamVoteActive" ) )
          vis = qfalse;
      }
      else if( team == TEAM_HUMANS )
      {
        if( trap_Cvar_VariableValue( "ui_humanTeamVoteActive" ) )
          vis = qfalse;
      }

      flags &= ~UI_SHOW_CANTEAMVOTE;
    }

    if( flags & UI_SHOW_FAVORITESERVERS )
    {
      // this assumes you only put this type of display flag on something showing in the proper context

      if( ui_netSource.integer != AS_FAVORITES )
        vis = qfalse;

      flags &= ~UI_SHOW_FAVORITESERVERS;
    }

    if( flags & UI_SHOW_NOTFAVORITESERVERS )
    {
      // this assumes you only put this type of display flag on something showing in the proper context

      if( ui_netSource.integer == AS_FAVORITES )
        vis = qfalse;

      flags &= ~UI_SHOW_NOTFAVORITESERVERS;
    }
    else
      flags = 0;
  }

  return vis;
}

static qboolean UI_NetSource_HandleKey( int key )
{
  if( key == K_MOUSE1 || key == K_MOUSE2 || key == K_ENTER || key == K_KP_ENTER )
  {
    if( key == K_MOUSE2 )
    {
      ui_netSource.integer--;

      if( ui_netSource.integer == AS_MPLAYER )
        ui_netSource.integer--;
    }
    else
    {
      ui_netSource.integer++;

      if( ui_netSource.integer == AS_MPLAYER )
        ui_netSource.integer++;
    }

    if( ui_netSource.integer >= numNetSources )
      ui_netSource.integer = 0;
    else if( ui_netSource.integer < 0 )
      ui_netSource.integer = numNetSources - 1;

    UI_BuildServerDisplayList( qtrue );

    if( ui_netSource.integer != AS_GLOBAL )
      UI_StartServerRefresh( qtrue );

    trap_Cvar_Set( "ui_netSource", va( "%d", ui_netSource.integer ) );
    return qtrue;
  }

  return qfalse;
}

static qboolean UI_OwnerDrawHandleKey( int ownerDraw, int key )
{
  switch( ownerDraw )
  {
    case UI_NETSOURCE:
      UI_NetSource_HandleKey( key );
      break;

    default:
      break;
  }

  return qfalse;
}


/*
=================
UI_ServersQsortCompare
=================
*/
static int QDECL UI_ServersQsortCompare( const void *arg1, const void *arg2 )
{
  return trap_LAN_CompareServers( ui_netSource.integer, uiInfo.serverStatus.sortKey,
                                  uiInfo.serverStatus.sortDir, *( int* )arg1, *( int* )arg2 );
}


/*
=================
UI_ServersSort
=================
*/
void UI_ServersSort( int column, qboolean force )
{
  if( !force )
  {
    if( uiInfo.serverStatus.sortKey == column )
      return;
  }

  uiInfo.serverStatus.sortKey = column;
  qsort( &uiInfo.serverStatus.displayServers[0], uiInfo.serverStatus.numDisplayServers,
          sizeof( int ), UI_ServersQsortCompare );
}

/*
===============
UI_LoadTeams
===============
*/
static void UI_LoadTeams( void )
{
  uiInfo.teamCount = 4;

  uiInfo.teamList[ 0 ].text = "Aliens";
  uiInfo.teamList[ 0 ].cmd = "cmd team aliens\n";
  uiInfo.teamList[ 0 ].type = INFOTYPE_TEXT;
  uiInfo.teamList[ 0 ].v.text =
    "The Alien Team\n\n"
    "The Aliens' strengths are in movement and the ability to "
    "quickly construct new bases quickly. They possess a range "
    "of abilities including basic melee attacks, movement-"
    "crippling poisons and more.";

  uiInfo.teamList[ 1 ].text = "Humans";
  uiInfo.teamList[ 1 ].cmd = "cmd team humans\n";
  uiInfo.teamList[ 1 ].type = INFOTYPE_TEXT;
  uiInfo.teamList[ 1 ].v.text =
    "The Human Team\n\n"
    "The humans are the masters of technology. Although their "
    "bases take long to construct, their automated defense "
    "ensures they stay built. A wide range of upgrades and "
    "weapons are available to the humans, each contributing "
    "to eradicate the alien threat.";

  uiInfo.teamList[ 2 ].text = "Spectate";
  uiInfo.teamList[ 2 ].cmd = "cmd team spectate\n";
  uiInfo.teamList[ 2 ].type = INFOTYPE_TEXT;
  uiInfo.teamList[ 2 ].v.text = "Watch the game without playing.";

  uiInfo.teamList[ 3 ].text = "Auto select";
  uiInfo.teamList[ 3 ].cmd = "cmd team auto\n";
  uiInfo.teamList[ 3 ].type = INFOTYPE_TEXT;
  uiInfo.teamList[ 3 ].v.text = "Join the team with the least players.";
}

/*
===============
UI_AddClass
===============
*/

static void UI_AddClass( class_t class )
{
  uiInfo.alienClassList[ uiInfo.alienClassCount ].text =
    BG_ClassConfig( class )->humanName;
  uiInfo.alienClassList[ uiInfo.alienClassCount ].cmd =
    String_Alloc( va( "cmd class %s\n", BG_Class( class )->name ) );
  uiInfo.alienClassList[ uiInfo.alienClassCount ].type = INFOTYPE_CLASS;

  uiInfo.alienClassList[ uiInfo.alienClassCount ].v.pclass = class;

  uiInfo.alienClassCount++;
}

/*
===============
UI_LoadAlienClasses
===============
*/
static void UI_LoadAlienClasses( void )
{
  uiInfo.alienClassCount = 0;

  if( BG_ClassIsAllowed( PCL_ALIEN_LEVEL0 ) )
    UI_AddClass( PCL_ALIEN_LEVEL0 );

  if( BG_ClassIsAllowed( PCL_ALIEN_BUILDER0_UPG ) &&
      BG_ClassAllowedInStage( PCL_ALIEN_BUILDER0_UPG, UI_GetCurrentAlienStage( ) ) )
    UI_AddClass( PCL_ALIEN_BUILDER0_UPG );
  else if( BG_ClassIsAllowed( PCL_ALIEN_BUILDER0 ) )
    UI_AddClass( PCL_ALIEN_BUILDER0 );
}

/*
===============
UI_AddItem
===============
*/
static void UI_AddItem( weapon_t weapon )
{
  uiInfo.humanItemList[ uiInfo.humanItemCount ].text =
    BG_Weapon( weapon )->humanName;
  uiInfo.humanItemList[ uiInfo.humanItemCount ].cmd =
    String_Alloc( va( "cmd class %s\n", BG_Weapon( weapon )->name ) );
  uiInfo.humanItemList[ uiInfo.humanItemCount ].type = INFOTYPE_WEAPON;
  uiInfo.humanItemList[ uiInfo.humanItemCount ].v.weapon = weapon;

  uiInfo.humanItemCount++;
}

/*
===============
UI_LoadHumanItems
===============
*/
static void UI_LoadHumanItems( void )
{
  uiInfo.humanItemCount = 0;

  if( BG_WeaponIsAllowed( WP_MACHINEGUN ) )
    UI_AddItem( WP_MACHINEGUN );

  if( BG_WeaponIsAllowed( WP_HBUILD ) )
    UI_AddItem( WP_HBUILD );
}

/*
===============
UI_ParseCarriageList
===============
*/
static void UI_ParseCarriageList( void )
{
  int  i;
  char carriageCvar[ MAX_TOKEN_CHARS ];
  char *iterator;
  char buffer[ MAX_TOKEN_CHARS ];
  char *bufPointer;

  trap_Cvar_VariableStringBuffer( "ui_carriage", carriageCvar, sizeof( carriageCvar ) );
  iterator = carriageCvar;

  uiInfo.weapons = 0;
  uiInfo.upgrades = 0;

  //simple parser to give rise to weapon/upgrade list

  while( iterator && iterator[ 0 ] != '$' )
  {
    bufPointer = buffer;

    if( iterator[ 0 ] == 'W' )
    {
      iterator++;

      while( iterator[ 0 ] != ' ' )
        *bufPointer++ = *iterator++;

      *bufPointer++ = '\n';

      i = atoi( buffer );

      uiInfo.weapons |= ( 1 << i );
    }
    else if( iterator[ 0 ] == 'U' )
    {
      iterator++;

      while( iterator[ 0 ] != ' ' )
        *bufPointer++ = *iterator++;

      *bufPointer++ = '\n';

      i = atoi( buffer );

      uiInfo.upgrades |= ( 1 << i );
    }

    iterator++;
  }
}

/*
===============
UI_LoadHumanArmouryBuys
===============
*/
static void UI_LoadHumanArmouryBuys( void )
{
  int i, j = 0;
  stage_t stage = UI_GetCurrentHumanStage( );
  int slots = 0;

  UI_ParseCarriageList( );

  for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
  {
    if( uiInfo.weapons & ( 1 << i ) )
      slots |= BG_Weapon( i )->slots;
  }

  for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
  {
    if( uiInfo.upgrades & ( 1 << i ) )
      slots |= BG_Upgrade( i )->slots;
  }

  uiInfo.humanArmouryBuyCount = 0;

  for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
  {
    if( BG_Weapon( i )->team == TEAM_HUMANS &&
        BG_Weapon( i )->purchasable &&
        BG_WeaponAllowedInStage( i, stage ) &&
        BG_WeaponIsAllowed( i ) &&
        !( BG_Weapon( i )->slots & slots ) &&
        !( uiInfo.weapons & ( 1 << i ) ) )
    {
      uiInfo.humanArmouryBuyList[ j ].text = BG_Weapon( i )->humanName;
      uiInfo.humanArmouryBuyList[ j ].cmd =
        String_Alloc( va( "cmd buy %s\n", BG_Weapon( i )->name ) );
      uiInfo.humanArmouryBuyList[ j ].type = INFOTYPE_WEAPON;
      uiInfo.humanArmouryBuyList[ j ].v.weapon = i;

      j++;

      uiInfo.humanArmouryBuyCount++;
    }
  }

  for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
  {
    if( BG_Upgrade( i )->team == TEAM_HUMANS &&
        BG_Upgrade( i )->purchasable &&
        BG_UpgradeAllowedInStage( i, stage ) &&
        BG_UpgradeIsAllowed( i ) &&
        !( BG_Upgrade( i )->slots & slots ) &&
        !( uiInfo.upgrades & ( 1 << i ) ) )
    {
      uiInfo.humanArmouryBuyList[ j ].text = BG_Upgrade( i )->humanName;
      uiInfo.humanArmouryBuyList[ j ].cmd =
        String_Alloc( va( "cmd buy %s\n", BG_Upgrade( i )->name ) );
      uiInfo.humanArmouryBuyList[ j ].type = INFOTYPE_UPGRADE;
      uiInfo.humanArmouryBuyList[ j ].v.upgrade = i;

      j++;

      uiInfo.humanArmouryBuyCount++;
    }
  }
}

/*
===============
UI_LoadHumanArmourySells
===============
*/
static void UI_LoadHumanArmourySells( void )
{
  int i, j = 0;

  uiInfo.humanArmourySellCount = 0;
  UI_ParseCarriageList( );

  for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
  {
    if( uiInfo.weapons & ( 1 << i ) )
    {
      uiInfo.humanArmourySellList[ j ].text = BG_Weapon( i )->humanName;
      uiInfo.humanArmourySellList[ j ].cmd =
        String_Alloc( va( "cmd sell %s\n", BG_Weapon( i )->name ) );
      uiInfo.humanArmourySellList[ j ].type = INFOTYPE_WEAPON;
      uiInfo.humanArmourySellList[ j ].v.weapon = i;

      j++;

      uiInfo.humanArmourySellCount++;
    }
  }

  for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
  {
    if( uiInfo.upgrades & ( 1 << i ) )
    {
      uiInfo.humanArmourySellList[ j ].text = BG_Upgrade( i )->humanName;
      uiInfo.humanArmourySellList[ j ].cmd =
        String_Alloc( va( "cmd sell %s\n", BG_Upgrade( i )->name ) );
      uiInfo.humanArmourySellList[ j ].type = INFOTYPE_UPGRADE;
      uiInfo.humanArmourySellList[ j ].v.upgrade = i;

      j++;

      uiInfo.humanArmourySellCount++;
    }
  }
}

/*
===============
UI_ArmouryRefreshCb
===============
*/
static void UI_ArmouryRefreshCb( void *data )
{
  int oldWeapons  = uiInfo.weapons;
  int oldUpgrades = uiInfo.upgrades;

  UI_ParseCarriageList( );

  if( uiInfo.weapons != oldWeapons || uiInfo.upgrades != oldUpgrades )
  {
    UI_LoadHumanArmouryBuys( );
    UI_LoadHumanArmourySells( );
    UI_RemoveCaptureFunc( );
  }
}

/*
===============
UI_LoadAlienUpgrades
===============
*/
static void UI_LoadAlienUpgrades( void )
{
  int     i, j = 0;

  int     class, credits;
  char    ui_currentClass[ MAX_STRING_CHARS ];
  stage_t stage = UI_GetCurrentAlienStage( );

  trap_Cvar_VariableStringBuffer( "ui_currentClass", ui_currentClass, MAX_STRING_CHARS );

  sscanf( ui_currentClass, "%d %d", &class, &credits );

  uiInfo.alienUpgradeCount = 0;

  for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ )
  {
    if( BG_ClassCanEvolveFromTo( class, i, credits, stage, 0 ) >= 0 )
    {
      uiInfo.alienUpgradeList[ j ].text = BG_ClassConfig( i )->humanName;
      uiInfo.alienUpgradeList[ j ].cmd =
        String_Alloc( va( "cmd class %s\n", BG_Class( i )->name ) );
      uiInfo.alienUpgradeList[ j ].type = INFOTYPE_CLASS;
      uiInfo.alienUpgradeList[ j ].v.pclass = i;

      j++;

      uiInfo.alienUpgradeCount++;
    }
  }
}

/*
===============
UI_LoadAlienBuilds
===============
*/
static void UI_LoadAlienBuilds( void )
{
  int     i, j = 0;
  stage_t stage;

  UI_ParseCarriageList( );
  stage = UI_GetCurrentAlienStage( );

  uiInfo.alienBuildCount = 0;

  for( i = BA_NONE + 1; i < BA_NUM_BUILDABLES; i++ )
  {
    if( BG_Buildable( i, NULL )->team == TEAM_ALIENS &&
        BG_Buildable( i, NULL )->buildWeapon & uiInfo.weapons &&
        BG_BuildableAllowedInStage( i, stage ) &&
        BG_BuildableIsAllowed( i ) )
    {
      uiInfo.alienBuildList[ j ].text = BG_Buildable( i, NULL )->humanName;
      uiInfo.alienBuildList[ j ].cmd =
        String_Alloc( va( "cmd build %s\n", BG_Buildable( i, NULL )->name ) );
      uiInfo.alienBuildList[ j ].type = INFOTYPE_BUILDABLE;
      uiInfo.alienBuildList[ j ].v.buildable = i;

      j++;

      uiInfo.alienBuildCount++;
    }
  }
}

/*
===============
UI_LoadHumanBuilds
===============
*/
static void UI_LoadHumanBuilds( void )
{
  int     i, j = 0;
  stage_t stage;
  static buildable_t list [ 14 ] =
  {
    BA_H_RTG,
    BA_H_REACTOR,
    BA_H_SPAWN,
    BA_H_REFINERY,
    BA_H_MGTURRET,
    BA_H_ARMOURY,
    BA_H_MEDISTAT,
    BA_H_REPEATER,
    BA_H_CAPBANK,
    BA_H_DCC,
    BA_H_TESLAGEN,
    BA_H_CUBOID1,
    BA_H_CUBOID2,
    BA_H_CUBOID3
  };

  UI_ParseCarriageList( );
  stage = UI_GetCurrentHumanStage( );

  uiInfo.humanBuildCount = 0;

  for( i = 0; i < 14; i++ )
  {
    if( BG_Buildable( list[ i ], NULL )->team == TEAM_HUMANS &&
        BG_Buildable( list[ i ], NULL )->buildWeapon & uiInfo.weapons &&
        BG_BuildableAllowedInStage( list[ i ], stage ) &&
        BG_BuildableIsAllowed( list[ i ] ) )
    {
      uiInfo.humanBuildList[ j ].text = BG_Buildable( list[ i ], NULL )->humanName;
      uiInfo.humanBuildList[ j ].cmd =
        String_Alloc( va( "cmd build %s\n", BG_Buildable( list[ i ], NULL )->name ) );
      uiInfo.humanBuildList[ j ].type = INFOTYPE_BUILDABLE;
      uiInfo.humanBuildList[ j ].v.buildable = list[ i ];

      j++;

      uiInfo.humanBuildCount++;
    }
  }
}

/*
===============
UI_LoadMods
===============
*/
static void UI_LoadMods( void )
{
  int   numdirs;
  char  dirlist[2048];
  char  *dirptr;
  char  *descptr;
  int   i;
  int   dirlen;

  uiInfo.modCount = 0;
  numdirs = trap_FS_GetFileList( "$modlist", "", dirlist, sizeof( dirlist ) );
  dirptr  = dirlist;

  for( i = 0; i < numdirs; i++ )
  {
    dirlen = strlen( dirptr ) + 1;
    descptr = dirptr + dirlen;
    uiInfo.modList[uiInfo.modCount].modName = String_Alloc( dirptr );
    uiInfo.modList[uiInfo.modCount].modDescr = String_Alloc( descptr );
    dirptr += dirlen + strlen( descptr ) + 1;
    uiInfo.modCount++;

    if( uiInfo.modCount >= MAX_MODS )
      break;
  }

}


/*
===============
UI_LoadMovies
===============
*/
static void UI_LoadMovies( void )
{
  char  movielist[4096];
  char  *moviename;
  int   i, len;

  uiInfo.movieCount = trap_FS_GetFileList( "video", "roq", movielist, 4096 );

  if( uiInfo.movieCount )
  {
    if( uiInfo.movieCount > MAX_MOVIES )
      uiInfo.movieCount = MAX_MOVIES;

    moviename = movielist;

    for( i = 0; i < uiInfo.movieCount; i++ )
    {
      len = strlen( moviename );

      if( !Q_stricmp( moviename +  len - 4, ".roq" ) )
        moviename[len-4] = '\0';

      Q_strupr( moviename );
      uiInfo.movieList[i] = String_Alloc( moviename );
      moviename += len + 1;
    }
  }

}



/*
===============
UI_LoadDemos
===============
*/
static void UI_LoadDemos( void )
{
  char  demolist[4096];
  char demoExt[32];
  char  *demoname;
  int   i, len;

  Com_sprintf( demoExt, sizeof( demoExt ), "dm_%d", ( int )trap_Cvar_VariableValue( "protocol" ) );

  uiInfo.demoCount = trap_FS_GetFileList( "demos", demoExt, demolist, 4096 );

  Com_sprintf( demoExt, sizeof( demoExt ), ".dm_%d", ( int )trap_Cvar_VariableValue( "protocol" ) );

  if( uiInfo.demoCount )
  {
    if( uiInfo.demoCount > MAX_DEMOS )
      uiInfo.demoCount = MAX_DEMOS;

    demoname = demolist;

    for( i = 0; i < uiInfo.demoCount; i++ )
    {
      len = strlen( demoname );

      if( !Q_stricmp( demoname +  len - strlen( demoExt ), demoExt ) )
        demoname[len-strlen( demoExt )] = '\0';

      uiInfo.demoList[i] = String_Alloc( demoname );
      demoname += len + 1;
    }
  }

}

static void UI_Update( const char *name )
{
  int val = trap_Cvar_VariableValue( name );

  if( Q_stricmp( name, "ui_SetName" ) == 0 )
    trap_Cvar_Set( "name", UI_Cvar_VariableString( "ui_Name" ) );
  else if( Q_stricmp( name, "ui_setRate" ) == 0 )
  {
    float rate = trap_Cvar_VariableValue( "rate" );

    if( rate >= 5000 )
    {
      trap_Cvar_Set( "cl_maxpackets", "30" );
      trap_Cvar_Set( "cl_packetdup", "1" );
    }
    else if( rate >= 4000 )
    {
      trap_Cvar_Set( "cl_maxpackets", "15" );
      trap_Cvar_Set( "cl_packetdup", "2" );   // favor less prediction errors when there's packet loss
    }
    else
    {
      trap_Cvar_Set( "cl_maxpackets", "15" );
      trap_Cvar_Set( "cl_packetdup", "1" );   // favor lower bandwidth
    }
  }
  else if( Q_stricmp( name, "ui_GetName" ) == 0 )
    trap_Cvar_Set( "ui_Name", UI_Cvar_VariableString( "name" ) );
  else if( Q_stricmp( name, "r_colorbits" ) == 0 )
  {
    switch( val )
    {
      case 0:
        trap_Cvar_SetValue( "r_depthbits", 0 );
        trap_Cvar_SetValue( "r_stencilbits", 0 );
        break;

      case 16:
        trap_Cvar_SetValue( "r_depthbits", 16 );
        trap_Cvar_SetValue( "r_stencilbits", 0 );
        break;

      case 32:
        trap_Cvar_SetValue( "r_depthbits", 24 );
        break;
    }
  }
  else if( Q_stricmp( name, "r_lodbias" ) == 0 )
  {
    switch( val )
    {
      case 0:
        trap_Cvar_SetValue( "r_subdivisions", 4 );
        break;

      case 1:
        trap_Cvar_SetValue( "r_subdivisions", 12 );
        break;

      case 2:
        trap_Cvar_SetValue( "r_subdivisions", 20 );
        break;
    }
  }
  else if( Q_stricmp( name, "ui_glCustom" ) == 0 )
  {
    switch( val )
    {
      case 0: // high quality
        trap_Cvar_SetValue( "r_subdivisions", 4 );
        trap_Cvar_SetValue( "r_vertexlight", 0 );
        trap_Cvar_SetValue( "r_lodbias", 0 );
        trap_Cvar_SetValue( "r_colorbits", 32 );
        trap_Cvar_SetValue( "r_depthbits", 24 );
        trap_Cvar_SetValue( "r_picmip", 0 );
        trap_Cvar_SetValue( "r_texturebits", 32 );
        trap_Cvar_SetValue( "r_fastSky", 0 );
        trap_Cvar_SetValue( "r_inGameVideo", 1 );
        trap_Cvar_SetValue( "cg_shadows", 1 );
        trap_Cvar_SetValue( "cg_bounceParticles", 1 );
        trap_Cvar_Set( "r_texturemode", "GL_LINEAR_MIPMAP_LINEAR" );
        break;

      case 1: // normal
        trap_Cvar_SetValue( "r_subdivisions", 12 );
        trap_Cvar_SetValue( "r_vertexlight", 0 );
        trap_Cvar_SetValue( "r_lodbias", 0 );
        trap_Cvar_SetValue( "r_colorbits", 0 );
        trap_Cvar_SetValue( "r_depthbits", 24 );
        trap_Cvar_SetValue( "r_picmip", 1 );
        trap_Cvar_SetValue( "r_texturebits", 0 );
        trap_Cvar_SetValue( "r_fastSky", 0 );
        trap_Cvar_SetValue( "r_inGameVideo", 1 );
        trap_Cvar_Set( "r_texturemode", "GL_LINEAR_MIPMAP_LINEAR" );
        trap_Cvar_SetValue( "cg_shadows", 0 );
        trap_Cvar_SetValue( "cg_bounceParticles", 0 );
        break;

      case 2: // fast
        trap_Cvar_SetValue( "r_subdivisions", 8 );
        trap_Cvar_SetValue( "r_vertexlight", 0 );
        trap_Cvar_SetValue( "r_lodbias", 1 );
        trap_Cvar_SetValue( "r_colorbits", 0 );
        trap_Cvar_SetValue( "r_depthbits", 0 );
        trap_Cvar_SetValue( "r_picmip", 1 );
        trap_Cvar_SetValue( "r_texturebits", 0 );
        trap_Cvar_SetValue( "cg_shadows", 0 );
        trap_Cvar_SetValue( "r_fastSky", 1 );
        trap_Cvar_SetValue( "r_inGameVideo", 0 );
        trap_Cvar_SetValue( "cg_bounceParticles", 0 );
        trap_Cvar_Set( "r_texturemode", "GL_LINEAR_MIPMAP_NEAREST" );
        break;

      case 3: // fastest
        trap_Cvar_SetValue( "r_subdivisions", 20 );
        trap_Cvar_SetValue( "r_vertexlight", 1 );
        trap_Cvar_SetValue( "r_lodbias", 2 );
        trap_Cvar_SetValue( "r_colorbits", 16 );
        trap_Cvar_SetValue( "r_depthbits", 16 );
        trap_Cvar_SetValue( "r_picmip", 2 );
        trap_Cvar_SetValue( "r_texturebits", 16 );
        trap_Cvar_SetValue( "cg_shadows", 0 );
        trap_Cvar_SetValue( "r_fastSky", 1 );
        trap_Cvar_SetValue( "r_inGameVideo", 0 );
        trap_Cvar_SetValue( "cg_bounceParticles", 0 );
        trap_Cvar_Set( "r_texturemode", "GL_LINEAR_MIPMAP_NEAREST" );
        break;
    }
  }
  else if( Q_stricmp( name, "ui_mousePitch" ) == 0 )
  {
    if( val == 0 )
      trap_Cvar_SetValue( "m_pitch", 0.022f );
    else
      trap_Cvar_SetValue( "m_pitch", -0.022f );
  }
}

//FIXME: lookup table
static void UI_RunMenuScript( char **args )
{
  const char *name, *name2;
  char buff[1024];
  const char *cmd;

  if( String_Parse( args, &name ) )
  {
    if( Q_stricmp( name, "StartServer" ) == 0 )
    {
      trap_Cvar_SetValue( "dedicated", Com_Clamp( 0, 2, ui_dedicated.integer ) );
      trap_Cmd_ExecuteText( EXEC_APPEND, va( "wait ; wait ; map %s\n",
                            uiInfo.mapList[ui_selectedMap.integer].mapLoadName ) );
    }
    else if( Q_stricmp( name, "resetDefaults" ) == 0 )
    {
      trap_Cmd_ExecuteText( EXEC_APPEND, "exec default.cfg\n" );
      trap_Cmd_ExecuteText( EXEC_APPEND, "cvar_restart\n" );
      Controls_SetDefaults();
      trap_Cvar_Set( "com_introPlayed", "1" );
      trap_Cmd_ExecuteText( EXEC_APPEND, "vid_restart\n" );
    }
    else if( Q_stricmp( name, "loadArenas" ) == 0 )
    {
      UI_LoadArenas();
      Menu_SetFeederSelection( NULL, FEEDER_MAPS, 0, "createserver" );
    }
    else if( Q_stricmp( name, "loadServerInfo" ) == 0 )
      UI_ServerInfo();
    else if( Q_stricmp( name, "getNews" ) == 0 )
      UI_UpdateNews( qtrue );
    else if( Q_stricmp( name, "saveControls" ) == 0 )
      Controls_SetConfig( qtrue );
    else if( Q_stricmp( name, "loadControls" ) == 0 )
      Controls_GetConfig();
    else if (Q_stricmp(name, "clearError") == 0) {
      trap_Cvar_Set("com_errorMessage", "");
    } else if (Q_stricmp(name, "downloadIgnore") == 0) {
      trap_Cvar_Set("com_downloadPrompt", va("%d", DLP_IGNORE));
    } else if (Q_stricmp(name, "downloadCURL") == 0) {
      trap_Cvar_Set("com_downloadPrompt", va("%d", DLP_CURL));
    } else if (Q_stricmp(name, "downloadUDP") == 0) {
      trap_Cvar_Set("com_downloadPrompt", va("%d", DLP_UDP));
    } else if (Q_stricmp(name, "RefreshServers") == 0) {
      UI_StartServerRefresh(qtrue);
      UI_BuildServerDisplayList(qtrue);
    } else if (Q_stricmp(name, "InitServerList") == 0) {
      int time = trap_RealTime( NULL );
      int last;
      int sortColumn;

      // set up default sorting

      if( !uiInfo.serverStatus.sorted && Int_Parse( args, &sortColumn ) )
      {
        uiInfo.serverStatus.sortKey = sortColumn;
        uiInfo.serverStatus.sortDir = 0;
      }

      // refresh if older than 3 days or if list is empty
      last = atoi( UI_Cvar_VariableString( va( "ui_lastServerRefresh_%i_time",
                                           ui_netSource.integer ) ) );

      if( trap_LAN_GetServerCount( ui_netSource.integer ) < 1 ||
          ( time - last ) > 3600 )
      {
        UI_StartServerRefresh( qtrue );
        UI_BuildServerDisplayList( qtrue );
      }
    }
    else if( Q_stricmp( name, "RefreshFilter" ) == 0 )
    {
      UI_StartServerRefresh( qfalse );
      UI_BuildServerDisplayList( qtrue );
    }
    else if( Q_stricmp( name, "LoadDemos" ) == 0 )
      UI_LoadDemos();
    else if( Q_stricmp( name, "LoadMovies" ) == 0 )
      UI_LoadMovies();
    else if( Q_stricmp( name, "LoadMods" ) == 0 )
      UI_LoadMods();
    else if( Q_stricmp( name, "LoadTeams" ) == 0 )
      UI_LoadTeams( );
    else if( Q_stricmp( name, "JoinTeam" ) == 0 )
    {
      if( ( cmd = uiInfo.teamList[ uiInfo.teamIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );
    }
    else if( Q_stricmp( name, "LoadHumanItems" ) == 0 )
      UI_LoadHumanItems( );
    else if( Q_stricmp( name, "SpawnWithHumanItem" ) == 0 )
    {
      if( ( cmd = uiInfo.humanItemList[ uiInfo.humanItemIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );
    }
    else if( Q_stricmp( name, "LoadAlienClasses" ) == 0 )
      UI_LoadAlienClasses( );
    else if( Q_stricmp( name, "SpawnAsAlienClass" ) == 0 )
    {
      if( ( cmd = uiInfo.alienClassList[ uiInfo.alienClassIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );
    }
    else if( Q_stricmp( name, "LoadHumanArmouryBuys" ) == 0 )
      UI_LoadHumanArmouryBuys( );
    else if( Q_stricmp( name, "BuyFromArmoury" ) == 0 )
    {
      if( ( cmd = uiInfo.humanArmouryBuyList[ uiInfo.humanArmouryBuyIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );

      UI_InstallCaptureFunc( UI_ArmouryRefreshCb, NULL, 1000 );
    }
    else if( Q_stricmp( name, "LoadHumanArmourySells" ) == 0 )
      UI_LoadHumanArmourySells( );
    else if( Q_stricmp( name, "SellToArmoury" ) == 0 )
    {
      if( ( cmd = uiInfo.humanArmourySellList[ uiInfo.humanArmourySellIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );

      UI_InstallCaptureFunc( UI_ArmouryRefreshCb, NULL, 1000 );
    }
    else if( Q_stricmp( name, "LoadAlienUpgrades" ) == 0 )
    {
      UI_LoadAlienUpgrades( );

      //disallow the menu if it would be empty

      if( uiInfo.alienUpgradeCount <= 0 )
        Menus_CloseAll( );
    }
    else if( Q_stricmp( name, "UpgradeToNewClass" ) == 0 )
    {
      if( ( cmd = uiInfo.alienUpgradeList[ uiInfo.alienUpgradeIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );
    }
    else if( Q_stricmp( name, "LoadAlienBuilds" ) == 0 )
      UI_LoadAlienBuilds( );
    else if( Q_stricmp( name, "BuildAlienBuildable" ) == 0 )
    {
      if( ( cmd = uiInfo.alienBuildList[ uiInfo.alienBuildIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );
    }
    else if( Q_stricmp( name, "LoadHumanBuilds" ) == 0 )
      UI_LoadHumanBuilds( );
    else if( Q_stricmp( name, "BuildHumanBuildable" ) == 0 )
    {
      if( ( cmd = uiInfo.humanBuildList[ uiInfo.humanBuildIndex ].cmd ) )
        trap_Cmd_ExecuteText( EXEC_APPEND, cmd );
    }
    else if( Q_stricmp( name, "Say" ) == 0 )
    {
      char buffer[ MAX_CVAR_VALUE_STRING ];
      trap_Cvar_VariableStringBuffer( "ui_sayBuffer", buffer, sizeof( buffer ) );

      if( !buffer[ 0 ] )
        ;
      else if( ui_chatCommands.integer && ( buffer[ 0 ] == '/' ||
        buffer[ 0 ] == '\\' ) )
      {
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "%s\n", buffer + 1 ) );
      }
      else if( uiInfo.chatTeam )
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "say_team \"%s\"\n", buffer ) );
      else
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "say \"%s\"\n", buffer ) );
    }
    else if( Q_stricmp( name, "SayKeydown" ) == 0 )
    {
      if( ui_chatCommands.integer )
      {
        char buffer[ MAX_CVAR_VALUE_STRING ];
        trap_Cvar_VariableStringBuffer( "ui_sayBuffer", buffer, sizeof( buffer ) );

        if( buffer[ 0 ] == '/' || buffer[ 0 ] == '\\' )
          Menus_ReplaceActiveByName( "say_command" );
        else if( uiInfo.chatTeam )
          Menus_ReplaceActiveByName( "say_team" );
        else
          Menus_ReplaceActiveByName( "say" );
      }
    }
    else if( Q_stricmp( name, "playMovie" ) == 0 )
    {
      if( uiInfo.previewMovie >= 0 )
        trap_CIN_StopCinematic( uiInfo.previewMovie );

      trap_Cmd_ExecuteText( EXEC_APPEND, va( "cinematic %s.roq 2\n", uiInfo.movieList[uiInfo.movieIndex] ) );
    }
    else if( Q_stricmp( name, "RunMod" ) == 0 )
    {
      trap_Cvar_Set( "fs_game", uiInfo.modList[uiInfo.modIndex].modName );
      trap_Cmd_ExecuteText( EXEC_APPEND, "vid_restart;" );
    }
    else if( Q_stricmp( name, "RunDemo" ) == 0 )
      trap_Cmd_ExecuteText( EXEC_APPEND, va( "demo %s\n", uiInfo.demoList[uiInfo.demoIndex] ) );
    else if( Q_stricmp( name, "Tremulous" ) == 0 )
    {
      trap_Cvar_Set( "fs_game", "" );
      trap_Cmd_ExecuteText( EXEC_APPEND, "vid_restart;" );
    }
    else if( Q_stricmp( name, "closeJoin" ) == 0 )
    {
      if( uiInfo.serverStatus.refreshActive )
      {
        UI_StopServerRefresh();
        uiInfo.serverStatus.nextDisplayRefresh = 0;
        uiInfo.nextServerStatusRefresh = 0;
        uiInfo.nextFindPlayerRefresh = 0;
        UI_BuildServerDisplayList( qtrue );
      }
      else
      {
        Menus_CloseByName( "joinserver" );
        Menus_ActivateByName( "main" );
      }
    }
    else if( Q_stricmp( name, "StopRefresh" ) == 0 )
    {
      UI_StopServerRefresh();
      uiInfo.serverStatus.nextDisplayRefresh = 0;
      uiInfo.nextServerStatusRefresh = 0;
      uiInfo.nextFindPlayerRefresh = 0;
    }
    else if( Q_stricmp( name, "UpdateFilter" ) == 0 )
    {
      if( ui_netSource.integer == AS_LOCAL )
        UI_StartServerRefresh( qtrue );

      UI_BuildServerDisplayList( qtrue );
      UI_FeederSelection( FEEDER_SERVERS, 0 );
    }
    else if( Q_stricmp( name, "ServerStatus" ) == 0 )
    {
      trap_LAN_GetServerAddressString( ui_netSource.integer,
                                       uiInfo.serverStatus.displayServers[uiInfo.serverStatus.currentServer],
                                       uiInfo.serverStatusAddress, sizeof( uiInfo.serverStatusAddress ) );
      UI_BuildServerStatus( qtrue );
    }
    else if( Q_stricmp( name, "FoundPlayerServerStatus" ) == 0 )
    {
      Q_strncpyz( uiInfo.serverStatusAddress,
                  uiInfo.foundPlayerServerAddresses[uiInfo.currentFoundPlayerServer],
                  sizeof( uiInfo.serverStatusAddress ) );
      UI_BuildServerStatus( qtrue );
      Menu_SetFeederSelection( NULL, FEEDER_FINDPLAYER, 0, NULL );
    }
    else if( Q_stricmp( name, "FindPlayer" ) == 0 )
    {
      UI_BuildFindPlayerList( qtrue );
      // clear the displayed server status info
      uiInfo.serverStatusInfo.numLines = 0;
      Menu_SetFeederSelection( NULL, FEEDER_FINDPLAYER, 0, NULL );
    }
    else if( Q_stricmp( name, "JoinServer" ) == 0 )
    {
      if( uiInfo.serverStatus.currentServer >= 0 &&
          uiInfo.serverStatus.currentServer < uiInfo.serverStatus.numDisplayServers )
      {
        trap_LAN_GetServerAddressString( ui_netSource.integer,
                                          uiInfo.serverStatus.displayServers[uiInfo.serverStatus.currentServer],
                                          buff, 1024 );
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "connect %s\n", buff ) );
      }
    }
    else if( Q_stricmp( name, "FoundPlayerJoinServer" ) == 0 )
    {
      if( uiInfo.currentFoundPlayerServer >= 0 &&
          uiInfo.currentFoundPlayerServer < uiInfo.numFoundPlayerServers )
      {
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "connect %s\n",
                              uiInfo.foundPlayerServerAddresses[uiInfo.currentFoundPlayerServer] ) );
      }
    }
    else if( Q_stricmp( name, "Quit" ) == 0 )
      trap_Cmd_ExecuteText( EXEC_APPEND, "quit" );
    else if( Q_stricmp( name, "Leave" ) == 0 )
    {
      trap_Cmd_ExecuteText( EXEC_APPEND, "disconnect\n" );
      trap_Key_SetCatcher( KEYCATCH_UI );
      Menus_CloseAll( );
      Menus_ActivateByName( "main" );
    }
    else if( Q_stricmp( name, "ServerSort" ) == 0 )
    {
      int sortColumn;

      if( Int_Parse( args, &sortColumn ) )
      {
        // if same column we're already sorting on then flip the direction

        if( sortColumn == uiInfo.serverStatus.sortKey )
          uiInfo.serverStatus.sortDir = !uiInfo.serverStatus.sortDir;

        // make sure we sort again
        UI_ServersSort( sortColumn, qtrue );

        uiInfo.serverStatus.sorted = qtrue;
      }
    }
    else if( Q_stricmp( name, "closeingame" ) == 0 )
    {
      trap_Key_SetCatcher( trap_Key_GetCatcher() & ~KEYCATCH_UI );
      trap_Key_ClearStates();
      trap_Cvar_Set( "cl_paused", "0" );
      Menus_CloseAll( );
    }
    else if( Q_stricmp( name, "voteMap" ) == 0 )
    {
      if( ui_selectedMap.integer >= 0 && ui_selectedMap.integer < uiInfo.mapCount )
      {
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callvote map %s\n",
                              uiInfo.mapList[ui_selectedMap.integer].mapLoadName ) );
      }
    }
    else if( Q_stricmp( name, "voteNextMap" ) == 0 )
    {
      if( ui_selectedMap.integer >= 0 && ui_selectedMap.integer < uiInfo.mapCount )
      {
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callvote nextmap %s\n",
                              uiInfo.mapList[ui_selectedMap.integer].mapLoadName ) );
      }
    }
    else if( Q_stricmp( name, "voteKick" ) == 0 )
    {
      if( uiInfo.playerIndex >= 0 && uiInfo.playerIndex < uiInfo.playerCount )
      {
        char buffer[ MAX_CVAR_VALUE_STRING ];
        trap_Cvar_VariableStringBuffer( "ui_reason", buffer, sizeof( buffer ) ); 

        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callvote kick %d %s\n",
                                               uiInfo.clientNums[ uiInfo.playerIndex ],
                                               buffer ) );
        trap_Cvar_Set( "ui_reason", "" );
      }
    }
    else if( Q_stricmp( name, "voteMute" ) == 0 )
    {
      if( uiInfo.playerIndex >= 0 && uiInfo.playerIndex < uiInfo.playerCount )
      {
        char buffer[ MAX_CVAR_VALUE_STRING ];
        trap_Cvar_VariableStringBuffer( "ui_reason", buffer, sizeof( buffer ) ); 

        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callvote mute %d %s\n",
                                               uiInfo.clientNums[ uiInfo.playerIndex ],
                                               buffer ) );
        trap_Cvar_Set( "ui_reason", "" );
      }
    }
    else if( Q_stricmp( name, "voteUnMute" ) == 0 )
    {
      if( uiInfo.playerIndex >= 0 && uiInfo.playerIndex < uiInfo.playerCount )
      {
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callvote unmute %d\n",
                                               uiInfo.clientNums[ uiInfo.playerIndex ] ) );
      }
    }
    else if( Q_stricmp( name, "voteTeamKick" ) == 0 )
    {
      if( uiInfo.teamPlayerIndex >= 0 && uiInfo.teamPlayerIndex < uiInfo.myTeamCount )
      {
        char buffer[ MAX_CVAR_VALUE_STRING ];
        trap_Cvar_VariableStringBuffer( "ui_reason", buffer, sizeof( buffer ) ); 

        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callteamvote kick %d %s\n",
                                               uiInfo.teamClientNums[ uiInfo.teamPlayerIndex ],
                                               buffer ) );
        trap_Cvar_Set( "ui_reason", "" );
      }
    }
    else if( Q_stricmp( name, "voteTeamDenyBuild" ) == 0 )
    {
      if( uiInfo.teamPlayerIndex >= 0 && uiInfo.teamPlayerIndex < uiInfo.myTeamCount )
      {
        char buffer[ MAX_CVAR_VALUE_STRING ];
        trap_Cvar_VariableStringBuffer( "ui_reason", buffer, sizeof( buffer ) ); 

        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callteamvote denybuild %d %s\n",
                                               uiInfo.teamClientNums[ uiInfo.teamPlayerIndex ],
                                               buffer ) );
        trap_Cvar_Set( "ui_reason", "" );
      }
    }
    else if( Q_stricmp( name, "voteTeamAllowBuild" ) == 0 )
    {
      if( uiInfo.teamPlayerIndex >= 0 && uiInfo.teamPlayerIndex < uiInfo.myTeamCount )
      {
        trap_Cmd_ExecuteText( EXEC_APPEND, va( "callteamvote allowbuild %d\n",
                                               uiInfo.teamClientNums[ uiInfo.teamPlayerIndex ] ) );
      }
    }
    else if( Q_stricmp( name, "addFavorite" ) == 0 )
    {
      if( ui_netSource.integer != AS_FAVORITES )
      {
        char name[MAX_NAME_LENGTH];
        char addr[MAX_NAME_LENGTH];
        int res;

        trap_LAN_GetServerInfo( ui_netSource.integer,
                                uiInfo.serverStatus.displayServers[uiInfo.serverStatus.currentServer],
                                buff, MAX_STRING_CHARS );
        name[0] = addr[0] = '\0';
        Q_strncpyz( name,  Info_ValueForKey( buff, "hostname" ), MAX_NAME_LENGTH );
        Q_strncpyz( addr,  Info_ValueForKey( buff, "addr" ), MAX_NAME_LENGTH );

        if( strlen( name ) > 0 && strlen( addr ) > 0 )
        {
          res = trap_LAN_AddServer( AS_FAVORITES, name, addr );

          if( res == 0 )
          {
            // server already in the list
            Com_Printf( "Favorite already in list\n" );
          }
          else if( res == -1 )
          {
            // list full
            Com_Printf( "Favorite list full\n" );
          }
          else
          {
            // successfully added
            Com_Printf( "Added favorite server %s\n", addr );
          }
        }
      }
    }
    else if( Q_stricmp( name, "deleteFavorite" ) == 0 )
    {
      if( ui_netSource.integer == AS_FAVORITES )
      {
        char addr[MAX_NAME_LENGTH];
        trap_LAN_GetServerInfo( ui_netSource.integer,
                                uiInfo.serverStatus.displayServers[uiInfo.serverStatus.currentServer],
                                buff, MAX_STRING_CHARS );
        addr[0] = '\0';
        Q_strncpyz( addr,  Info_ValueForKey( buff, "addr" ), MAX_NAME_LENGTH );

        if( strlen( addr ) > 0 )
          trap_LAN_RemoveServer( AS_FAVORITES, addr );
      }
    }
    else if( Q_stricmp( name, "createFavorite" ) == 0 )
    {
      if( ui_netSource.integer == AS_FAVORITES )
      {
        char name[MAX_NAME_LENGTH];
        char addr[MAX_NAME_LENGTH];
        int res;

        name[0] = addr[0] = '\0';
        Q_strncpyz( name,  UI_Cvar_VariableString( "ui_favoriteName" ), MAX_NAME_LENGTH );
        Q_strncpyz( addr,  UI_Cvar_VariableString( "ui_favoriteAddress" ), MAX_NAME_LENGTH );

        if( strlen( name ) > 0 && strlen( addr ) > 0 )
        {
          res = trap_LAN_AddServer( AS_FAVORITES, name, addr );

          if( res == 0 )
          {
            // server already in the list
            Com_Printf( "Favorite already in list\n" );
          }
          else if( res == -1 )
          {
            // list full
            Com_Printf( "Favorite list full\n" );
          }
          else
          {
            // successfully added
            Com_Printf( "Added favorite server %s\n", addr );
          }
        }
      }
    }
    else if( Q_stricmp( name, "glCustom" ) == 0 )
      trap_Cvar_Set( "ui_glCustom", "4" );
    else if( Q_stricmp( name, "update" ) == 0 )
    {
      if( String_Parse( args, &name2 ) )
        UI_Update( name2 );
    }
    else if( Q_stricmp( name, "InitIgnoreList" ) == 0 )
      UI_BuildPlayerList();
    else if( Q_stricmp( name, "ToggleIgnore" ) == 0 )
    {
      if( uiInfo.ignoreIndex >= 0 && uiInfo.ignoreIndex < uiInfo.playerCount )
      {
        if( Com_ClientListContains( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                               uiInfo.clientNums[ uiInfo.ignoreIndex ] ) )
        {
          Com_ClientListRemove( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                               uiInfo.clientNums[ uiInfo.ignoreIndex ] );
          trap_Cmd_ExecuteText( EXEC_APPEND, va( "unignore %i\n",
                                              uiInfo.clientNums[ uiInfo.ignoreIndex ] ) );
        }
        else
        {
          Com_ClientListAdd( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                            uiInfo.clientNums[ uiInfo.ignoreIndex ] );
          trap_Cmd_ExecuteText( EXEC_APPEND, va( "ignore %i\n",
                                              uiInfo.clientNums[ uiInfo.ignoreIndex ] ) );
        }
      }
    }
    else if( Q_stricmp( name, "IgnorePlayer" ) == 0 )
    {
      if( uiInfo.ignoreIndex >= 0 && uiInfo.ignoreIndex < uiInfo.playerCount )
      {
        if( !Com_ClientListContains( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                                uiInfo.clientNums[ uiInfo.ignoreIndex ] ) )
        {
          Com_ClientListAdd( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                            uiInfo.clientNums[ uiInfo.ignoreIndex ] );
          trap_Cmd_ExecuteText( EXEC_APPEND, va( "ignore %i\n",
                                              uiInfo.clientNums[ uiInfo.ignoreIndex ] ) );
        }
      }
    }
    else if( Q_stricmp( name, "UnIgnorePlayer" ) == 0 )
    {
      if( uiInfo.ignoreIndex >= 0 && uiInfo.ignoreIndex < uiInfo.playerCount )
      {
        if( Com_ClientListContains( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                               uiInfo.clientNums[ uiInfo.ignoreIndex ] ) )
        {
          Com_ClientListRemove( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                               uiInfo.clientNums[ uiInfo.ignoreIndex ] );
          trap_Cmd_ExecuteText( EXEC_APPEND, va( "unignore %i\n",
                                              uiInfo.clientNums[ uiInfo.ignoreIndex ] ) );
        }
      }
    }
    else
      Com_Printf( "unknown UI script %s\n", name );
  }
}

static int UI_FeederInitialise( int feederID );

/*
==================
UI_FeederCount
==================
*/
static int UI_FeederCount( int feederID )
{
  if( feederID == FEEDER_CINEMATICS )
    return uiInfo.movieCount;
  else if( feederID == FEEDER_MAPS )
    return uiInfo.mapCount;
  else if( feederID == FEEDER_SERVERS )
    return uiInfo.serverStatus.numDisplayServers;
  else if( feederID == FEEDER_SERVERSTATUS )
    return uiInfo.serverStatusInfo.numLines;
  else if( feederID == FEEDER_NEWS )
    return uiInfo.newsInfo.numLines;
  else if( feederID == FEEDER_FINDPLAYER )
    return uiInfo.numFoundPlayerServers;
  else if( feederID == FEEDER_PLAYER_LIST )
  {
    if( uiInfo.uiDC.realTime > uiInfo.playerRefresh )
    {
      uiInfo.playerRefresh = uiInfo.uiDC.realTime + 3000;
      UI_BuildPlayerList();
    }

    return uiInfo.playerCount;
  }
  else if( feederID == FEEDER_TEAM_LIST )
  {
    if( uiInfo.uiDC.realTime > uiInfo.playerRefresh )
    {
      uiInfo.playerRefresh = uiInfo.uiDC.realTime + 3000;
      UI_BuildPlayerList();
    }

    return uiInfo.myTeamCount;
  }
  else if( feederID == FEEDER_IGNORE_LIST )
    return uiInfo.playerCount;
  else if( feederID == FEEDER_HELP_LIST )
    return uiInfo.helpCount;
  else if( feederID == FEEDER_MODS )
    return uiInfo.modCount;
  else if( feederID == FEEDER_DEMOS )
    return uiInfo.demoCount;
  else if( feederID == FEEDER_TREMTEAMS )
    return uiInfo.teamCount;
  else if( feederID == FEEDER_TREMHUMANITEMS )
    return uiInfo.humanItemCount;
  else if( feederID == FEEDER_TREMALIENCLASSES )
    return uiInfo.alienClassCount;
  else if( feederID == FEEDER_TREMHUMANARMOURYBUY )
    return uiInfo.humanArmouryBuyCount;
  else if( feederID == FEEDER_TREMHUMANARMOURYSELL )
    return uiInfo.humanArmourySellCount;
  else if( feederID == FEEDER_TREMALIENUPGRADE )
    return uiInfo.alienUpgradeCount;
  else if( feederID == FEEDER_TREMALIENBUILD )
    return uiInfo.alienBuildCount;
  else if( feederID == FEEDER_TREMHUMANBUILD )
    return uiInfo.humanBuildCount;
  else if( feederID == FEEDER_RESOLUTIONS )
  {
    if( UI_FeederInitialise( feederID ) == uiInfo.numResolutions )
      return uiInfo.numResolutions + 1;
    else
      return uiInfo.numResolutions;
  }

  return 0;
}

static const char *UI_SelectedMap( int index, int *actual )
{
  int i, c;
  c = 0;
  *actual = 0;

  for( i = 0; i < uiInfo.mapCount; i++ )
  {
    if( c == index )
    {
      *actual = i;
      return uiInfo.mapList[i].mapName;
    }
    else
      c++;
  }

  return "";
}

static int GCD( int a, int b )
{
  int c;

  while( b != 0 )
  {
    c = a % b;
    a = b;
    b = c;
  }

  return a;
}

static const char *UI_DisplayAspectString( int w, int h )
{
  int gcd = GCD( w, h );

  w /= gcd;
  h /= gcd;

  // For some reason 8:5 is usually referred to as 16:10
  if( w == 8 && h == 5 )
  {
    w = 16;
    h = 10;
  }

  return va( "%d:%d", w, h );
}

static const char *UI_FeederItemText( int feederID, int index, int column, qhandle_t *handle )
{
  if( handle )
    *handle = -1;

  if( feederID == FEEDER_MAPS )
  {
    int actual;
    return UI_SelectedMap( index, &actual );
  }
  else if( feederID == FEEDER_SERVERS )
  {
    if( index >= 0 && index < UI_FeederCount( feederID ) )
    {
      static char info[MAX_STRING_CHARS];
      static char clientBuff[ 32 ];
      static char cleaned[ MAX_STRING_CHARS ];
      static int  lastColumn = -1;
      static int  lastTime = 0;
      int         ping;

      if( lastColumn != column || lastTime > uiInfo.uiDC.realTime + 5000 )
      {
        trap_LAN_GetServerInfo( ui_netSource.integer, uiInfo.serverStatus.displayServers[index],
                                info, MAX_STRING_CHARS );
        lastColumn = column;
        lastTime = uiInfo.uiDC.realTime;
      }

      ping = atoi( Info_ValueForKey( info, "ping" ) );

      UI_EscapeEmoticons( cleaned, Info_ValueForKey( info, "hostname" ), sizeof( cleaned ) );

      switch( column )
      {
        case SORT_HOST:
          if( ping <= 0 )
            return Info_ValueForKey( info, "addr" );
          else
          {
            static char hostname[1024];

            if( ui_netSource.integer == AS_LOCAL )
            {
              Com_sprintf( hostname, sizeof( hostname ), "%s [%s]", cleaned, 
                           netnames[atoi( Info_ValueForKey( info, "nettype" ) )] );
              return hostname;
            }
            else
            {
              char *text;
              char *label;

              label = Info_ValueForKey( info, "label" );
              if( label[0] )
              {
                // First char of the label response is a sorting tag. Skip it.
                label+= 1;

                Com_sprintf( hostname, sizeof( hostname ), "%s %s", 
                             label, cleaned );
              }
              else
              {
                Com_sprintf( hostname, sizeof( hostname ), "%s", 
                             cleaned );
              }

              // Strip leading whitespace
              text = hostname;

              while( *text != '\0' && *text == ' ' )
                text++;

              return text;
            }
          }

        case SORT_GAME:
          return Info_ValueForKey( info, "game" );

        case SORT_MAP:
          return Info_ValueForKey( info, "mapname" );

        case SORT_CLIENTS:
          Com_sprintf( clientBuff, sizeof( clientBuff ), "%s (%s)",
                       Info_ValueForKey( info, "clients" ), Info_ValueForKey( info, "sv_maxclients" ) );
          return clientBuff;

        case SORT_PING:
          if( ping <= 0 )
            return "...";
          else
            return Info_ValueForKey( info, "ping" );
      }
    }
  }
  else if( feederID == FEEDER_SERVERSTATUS )
  {
    if( index >= 0 && index < uiInfo.serverStatusInfo.numLines )
    {
      if( column >= 0 && column < 4 )
        return uiInfo.serverStatusInfo.lines[index][column];
    }
  }
  else if( feederID == FEEDER_NEWS )
  {
    if( index >= 0 && index < uiInfo.newsInfo.numLines )
      return uiInfo.newsInfo.text[index];
  }
  else if( feederID == FEEDER_FINDPLAYER )
  {
    if( index >= 0 && index < uiInfo.numFoundPlayerServers )
      return uiInfo.foundPlayerServerNames[index];
  }
  else if( feederID == FEEDER_PLAYER_LIST )
  {
    if( index >= 0 && index < uiInfo.playerCount )
      return uiInfo.playerNames[index];
  }
  else if( feederID == FEEDER_TEAM_LIST )
  {
    if( index >= 0 && index < uiInfo.myTeamCount )
      return uiInfo.teamNames[index];
  }
  else if( feederID == FEEDER_IGNORE_LIST )
  {
    if( index >= 0 && index < uiInfo.playerCount )
    {
      switch( column )
      {
        case 1:
          // am I ignoring him
          return Com_ClientListContains( &uiInfo.ignoreList[ uiInfo.myPlayerIndex ],
                                         uiInfo.clientNums[ index ] ) ? "X" : "";

        case 2:
          // is he ignoring me
          return Com_ClientListContains( &uiInfo.ignoreList[ index ],
                                         uiInfo.playerNumber ) ? "X" : "";

        default:
          return uiInfo.playerNames[index];
      }
    }
  }
  else if( feederID == FEEDER_HELP_LIST )
  {
    if( index >= 0 && index < uiInfo.helpCount )
      return uiInfo.helpList[ index ].text;
  }
  else if( feederID == FEEDER_MODS )
  {
    if( index >= 0 && index < uiInfo.modCount )
    {
      if( uiInfo.modList[index].modDescr && *uiInfo.modList[index].modDescr )
        return uiInfo.modList[index].modDescr;
      else
        return uiInfo.modList[index].modName;
    }
  }
  else if( feederID == FEEDER_CINEMATICS )
  {
    if( index >= 0 && index < uiInfo.movieCount )
      return uiInfo.movieList[index];
  }
  else if( feederID == FEEDER_DEMOS )
  {
    if( index >= 0 && index < uiInfo.demoCount )
      return uiInfo.demoList[index];
  }
  else if( feederID == FEEDER_TREMTEAMS )
  {
    if( index >= 0 && index < uiInfo.teamCount )
      return uiInfo.teamList[ index ].text;
  }
  else if( feederID == FEEDER_TREMHUMANITEMS )
  {
    if( index >= 0 && index < uiInfo.humanItemCount )
      return uiInfo.humanItemList[ index ].text;
  }
  else if( feederID == FEEDER_TREMALIENCLASSES )
  {
    if( index >= 0 && index < uiInfo.alienClassCount )
      return uiInfo.alienClassList[ index ].text;
  }
  else if( feederID == FEEDER_TREMHUMANARMOURYBUY )
  {
    if( index >= 0 && index < uiInfo.humanArmouryBuyCount )
      return uiInfo.humanArmouryBuyList[ index ].text;
  }
  else if( feederID == FEEDER_TREMHUMANARMOURYSELL )
  {
    if( index >= 0 && index < uiInfo.humanArmourySellCount )
      return uiInfo.humanArmourySellList[ index ].text;
  }
  else if( feederID == FEEDER_TREMALIENUPGRADE )
  {
    if( index >= 0 && index < uiInfo.alienUpgradeCount )
      return uiInfo.alienUpgradeList[ index ].text;
  }
  else if( feederID == FEEDER_TREMALIENBUILD )
  {
    if( index >= 0 && index < uiInfo.alienBuildCount )
      return uiInfo.alienBuildList[ index ].text;
  }
  else if( feederID == FEEDER_TREMHUMANBUILD )
  {
    if( index >= 0 && index < uiInfo.humanBuildCount )
      return uiInfo.humanBuildList[ index ].text;
  }
  else if( feederID == FEEDER_RESOLUTIONS )
  {
    static char resolution[MAX_STRING_CHARS];
    int w, h;

    if( index >= 0 && index < uiInfo.numResolutions )
    {
      w = uiInfo.resolutions[ index ].w;
      h = uiInfo.resolutions[ index ].h;

      Com_sprintf( resolution, sizeof( resolution ), "%dx%d (%s)", w, h,
          UI_DisplayAspectString( w, h ) );

      return resolution;
    }

    w = (int)trap_Cvar_VariableValue( "r_width" );
    h = (int)trap_Cvar_VariableValue( "r_height" );
    Com_sprintf( resolution, sizeof( resolution ), "Custom (%dx%d)", w, h );

    return resolution;
  }

  return "";
}


static qhandle_t UI_FeederItemImage( int feederID, int index )
{
  if( feederID == FEEDER_MAPS )
  {
    int actual;
    UI_SelectedMap( index, &actual );
    index = actual;

    if( index >= 0 && index < uiInfo.mapCount )
    {
      if( uiInfo.mapList[index].levelShot == -1 )
        uiInfo.mapList[index].levelShot = trap_R_RegisterShaderNoMip( uiInfo.mapList[index].imageName );

      return uiInfo.mapList[index].levelShot;
    }
  }

  return 0;
}

static void UI_FeederSelection( int feederID, int index )
{
  static char info[MAX_STRING_CHARS];

  if( feederID == FEEDER_MAPS )
  {
    int actual, map;
    map = ui_selectedMap.integer;

    if( uiInfo.mapList[map].cinematic >= 0 )
    {
      trap_CIN_StopCinematic( uiInfo.mapList[map].cinematic );
      uiInfo.mapList[map].cinematic = -1;
    }

    UI_SelectedMap( index, &actual );

    ui_selectedMap.integer = actual;
    trap_Cvar_Set( "ui_selectedMap", va( "%d", actual ) );
    uiInfo.mapList[ui_selectedMap.integer].cinematic =
      trap_CIN_PlayCinematic( va( "%s.roq", uiInfo.mapList[ui_selectedMap.integer].mapLoadName ),
                              0, 0, 0, 0, ( CIN_loop | CIN_silent ) );
  }
  else if( feederID == FEEDER_SERVERS )
  {
    const char *mapName = NULL;

    uiInfo.serverStatus.currentServer = index;
    trap_LAN_GetServerInfo( ui_netSource.integer, uiInfo.serverStatus.displayServers[index],
                            info, MAX_STRING_CHARS );
    uiInfo.serverStatus.currentServerPreview =
      trap_R_RegisterShaderNoMip( va( "levelshots/%s", Info_ValueForKey( info, "mapname" ) ) );

    if( uiInfo.serverStatus.currentServerCinematic >= 0 )
    {
      trap_CIN_StopCinematic( uiInfo.serverStatus.currentServerCinematic );
      uiInfo.serverStatus.currentServerCinematic = -1;
    }

    mapName = Info_ValueForKey( info, "mapname" );

    if( mapName && *mapName )
    {
      uiInfo.serverStatus.currentServerCinematic =
        trap_CIN_PlayCinematic( va( "%s.roq", mapName ), 0, 0, 0, 0, ( CIN_loop | CIN_silent ) );
    }
  }
  else if( feederID == FEEDER_SERVERSTATUS )
  {
  }
  else if( feederID == FEEDER_FINDPLAYER )
  {
    uiInfo.currentFoundPlayerServer = index;
    //

    if( index < uiInfo.numFoundPlayerServers - 1 )
    {
      // build a new server status for this server
      Q_strncpyz( uiInfo.serverStatusAddress,
                  uiInfo.foundPlayerServerAddresses[uiInfo.currentFoundPlayerServer],
                  sizeof( uiInfo.serverStatusAddress ) );
      Menu_SetFeederSelection( NULL, FEEDER_SERVERSTATUS, 0, NULL );
      UI_BuildServerStatus( qtrue );
    }
  }
  else if( feederID == FEEDER_PLAYER_LIST )
    uiInfo.playerIndex = index;
  else if( feederID == FEEDER_TEAM_LIST )
    uiInfo.teamPlayerIndex = index;
  else if( feederID == FEEDER_IGNORE_LIST )
    uiInfo.ignoreIndex = index;
  else if( feederID == FEEDER_HELP_LIST )
    uiInfo.helpIndex = index;
  else if( feederID == FEEDER_MODS )
    uiInfo.modIndex = index;
  else if( feederID == FEEDER_CINEMATICS )
  {
    uiInfo.movieIndex = index;

    if( uiInfo.previewMovie >= 0 )
      trap_CIN_StopCinematic( uiInfo.previewMovie );

    uiInfo.previewMovie = -1;
  }
  else if( feederID == FEEDER_DEMOS )
    uiInfo.demoIndex = index;
  else if( feederID == FEEDER_TREMTEAMS )
    uiInfo.teamIndex = index;
  else if( feederID == FEEDER_TREMHUMANITEMS )
    uiInfo.humanItemIndex = index;
  else if( feederID == FEEDER_TREMALIENCLASSES )
    uiInfo.alienClassIndex = index;
  else if( feederID == FEEDER_TREMHUMANARMOURYBUY )
    uiInfo.humanArmouryBuyIndex = index;
  else if( feederID == FEEDER_TREMHUMANARMOURYSELL )
    uiInfo.humanArmourySellIndex = index;
  else if( feederID == FEEDER_TREMALIENUPGRADE )
    uiInfo.alienUpgradeIndex = index;
  else if( feederID == FEEDER_TREMALIENBUILD )
    uiInfo.alienBuildIndex = index;
  else if( feederID == FEEDER_TREMHUMANBUILD )
    uiInfo.humanBuildIndex = index;
  else if( feederID == FEEDER_RESOLUTIONS )
  {
    if( index >= 0 && index < uiInfo.numResolutions )
    {
      trap_Cvar_Set( "r_width", va( "%d", uiInfo.resolutions[ index ].w ) );
      trap_Cvar_Set( "r_height", va( "%d", uiInfo.resolutions[ index ].h ) );
    }

    uiInfo.resolutionIndex = index;
  }
}

static int UI_FeederInitialise( int feederID )
{
  if( feederID == FEEDER_RESOLUTIONS )
  {
    int i;
    int w = trap_Cvar_VariableValue( "r_width" );
    int h = trap_Cvar_VariableValue( "r_height" );

    for( i = 0; i < uiInfo.numResolutions; i++ )
    {
      if( w == uiInfo.resolutions[ i ].w && h == uiInfo.resolutions[ i ].h )
        return i;
    }

    return uiInfo.numResolutions;
  }

  return 0;
}

static void UI_Pause( qboolean b )
{
  if( b )
  {
    // pause the game and set the ui keycatcher
    trap_Cvar_Set( "cl_paused", "1" );
    trap_Key_SetCatcher( KEYCATCH_UI );
  }
  else
  {
    // unpause the game and clear the ui keycatcher
    trap_Key_SetCatcher( trap_Key_GetCatcher() & ~KEYCATCH_UI );
    trap_Key_ClearStates();
    trap_Cvar_Set( "cl_paused", "0" );
  }
}

static int UI_PlayCinematic( const char *name, float x, float y, float w, float h )
{
  return trap_CIN_PlayCinematic( name, x, y, w, h, ( CIN_loop | CIN_silent ) );
}

static void UI_StopCinematic( int handle )
{
  if( handle >= 0 )
    trap_CIN_StopCinematic( handle );
  else
  {
    handle = abs( handle );

    if( handle == UI_NETMAPCINEMATIC )
    {
      if( uiInfo.serverStatus.currentServerCinematic >= 0 )
      {
        trap_CIN_StopCinematic( uiInfo.serverStatus.currentServerCinematic );
        uiInfo.serverStatus.currentServerCinematic = -1;
      }
    }
  }
}

static void UI_DrawCinematic( int handle, float x, float y, float w, float h )
{
  trap_CIN_SetExtents( handle, x, y, w, h );
  trap_CIN_DrawCinematic( handle );
}

static void UI_RunCinematicFrame( int handle )
{
  trap_CIN_RunCinematic( handle );
}

static float UI_GetValue( int ownerDraw )
{
  return 0.0f;
}

/*
=================
UI_ParseResolutions
=================
*/
void UI_ParseResolutions( void )
{
  char        buf[ MAX_STRING_CHARS ];
  char        w[ 16 ], h[ 16 ];
  char        *p;
  const char  *out;
  char        *s = NULL;

  trap_Cvar_VariableStringBuffer( "r_availableModes", buf, sizeof( buf ) );
  p = buf;
  uiInfo.numResolutions = 0;

  while( String_Parse( &p, &out ) )
  {
    Q_strncpyz( w, out, sizeof( w ) );
    s = strchr( w, 'x' );
    if( !s )
      return;

    *s++ = '\0';
    Q_strncpyz( h, s, sizeof( h ) );

    uiInfo.resolutions[ uiInfo.numResolutions ].w = atoi( w );
    uiInfo.resolutions[ uiInfo.numResolutions ].h = atoi( h );
    uiInfo.numResolutions++;
  }
}

/*
=================
UI_Init
=================
*/
void UI_Init( qboolean inGameLoad )
{
  int start;

  BG_InitClassConfigs( );
  BG_InitAllowedGameElements( );

  uiInfo.inGameLoad = inGameLoad;

  UI_RegisterCvars();
  UI_InitMemory();

  // cache redundant calulations
  trap_GetGlconfig( &uiInfo.uiDC.glconfig );

  // for 640x480 virtualized screen
  uiInfo.uiDC.yscale = uiInfo.uiDC.glconfig.vidHeight * ( 1.0f / 480.0f );
  uiInfo.uiDC.xscale = uiInfo.uiDC.glconfig.vidWidth * ( 1.0f / 640.0f );

  // wide screen
  uiInfo.uiDC.aspectScale = ( ( 640.0f * uiInfo.uiDC.glconfig.vidHeight ) /
      ( 480.0f * uiInfo.uiDC.glconfig.vidWidth ) );

  uiInfo.uiDC.smallFontScale = trap_Cvar_VariableValue( "ui_smallFont" );
  uiInfo.uiDC.bigFontScale = trap_Cvar_VariableValue( "ui_bigFont" );

  uiInfo.uiDC.registerShaderNoMip = &trap_R_RegisterShaderNoMip;
  uiInfo.uiDC.setColor = &UI_SetColor;
  uiInfo.uiDC.drawHandlePic = &UI_DrawHandlePic;
  uiInfo.uiDC.drawStretchPic = &trap_R_DrawStretchPic;
  uiInfo.uiDC.registerModel = &trap_R_RegisterModel;
  uiInfo.uiDC.modelBounds = &trap_R_ModelBounds;
  uiInfo.uiDC.fillRect = &UI_FillRect;
  uiInfo.uiDC.drawRect = &UI_DrawRect;
  uiInfo.uiDC.drawSides = &UI_DrawSides;
  uiInfo.uiDC.drawTopBottom = &UI_DrawTopBottom;
  uiInfo.uiDC.clearScene = &trap_R_ClearScene;
  uiInfo.uiDC.drawSides = &UI_DrawSides;
  uiInfo.uiDC.addRefEntityToScene = &trap_R_AddRefEntityToScene;
  uiInfo.uiDC.renderScene = &trap_R_RenderScene;
  uiInfo.uiDC.registerFont = &trap_R_RegisterFont;
  uiInfo.uiDC.ownerDrawItem = &UI_OwnerDraw;
  uiInfo.uiDC.getValue = &UI_GetValue;
  uiInfo.uiDC.ownerDrawVisible = &UI_OwnerDrawVisible;
  uiInfo.uiDC.runScript = &UI_RunMenuScript;
  uiInfo.uiDC.setCVar = trap_Cvar_Set;
  uiInfo.uiDC.getCVarString = trap_Cvar_VariableStringBuffer;
  uiInfo.uiDC.getCVarValue = trap_Cvar_VariableValue;
  uiInfo.uiDC.setOverstrikeMode = &trap_Key_SetOverstrikeMode;
  uiInfo.uiDC.getOverstrikeMode = &trap_Key_GetOverstrikeMode;
  uiInfo.uiDC.startLocalSound = &trap_S_StartLocalSound;
  uiInfo.uiDC.ownerDrawHandleKey = &UI_OwnerDrawHandleKey;
  uiInfo.uiDC.feederCount = &UI_FeederCount;
  uiInfo.uiDC.feederItemImage = &UI_FeederItemImage;
  uiInfo.uiDC.feederItemText = &UI_FeederItemText;
  uiInfo.uiDC.feederSelection = &UI_FeederSelection;
  uiInfo.uiDC.feederInitialise = &UI_FeederInitialise;
  uiInfo.uiDC.setBinding = &trap_Key_SetBinding;
  uiInfo.uiDC.getBindingBuf = &trap_Key_GetBindingBuf;
  uiInfo.uiDC.keynumToStringBuf = &trap_Key_KeynumToStringBuf;
  uiInfo.uiDC.executeText = &trap_Cmd_ExecuteText;
  uiInfo.uiDC.Error = &Com_Error;
  uiInfo.uiDC.Print = &Com_Printf;
  uiInfo.uiDC.Pause = &UI_Pause;
  uiInfo.uiDC.ownerDrawWidth = &UI_OwnerDrawWidth;
  uiInfo.uiDC.ownerDrawText = &UI_OwnerDrawText;
  uiInfo.uiDC.registerSound = &trap_S_RegisterSound;
  uiInfo.uiDC.startBackgroundTrack = &trap_S_StartBackgroundTrack;
  uiInfo.uiDC.stopBackgroundTrack = &trap_S_StopBackgroundTrack;
  uiInfo.uiDC.playCinematic = &UI_PlayCinematic;
  uiInfo.uiDC.stopCinematic = &UI_StopCinematic;
  uiInfo.uiDC.drawCinematic = &UI_DrawCinematic;
  uiInfo.uiDC.runCinematicFrame = &UI_RunCinematicFrame;

  Init_Display( &uiInfo.uiDC );

  String_Init();

  uiInfo.uiDC.whiteShader = trap_R_RegisterShaderNoMip( "white" );

  AssetCache();

  start = trap_Milliseconds();

  UI_LoadMenus( "ui/menus.txt", qtrue );
  UI_LoadMenus( "ui/ingame.txt", qfalse );
  UI_LoadMenus( "ui/tremulous.txt", qfalse );
  UI_LoadHelp( "ui/help.txt" );

  Menus_CloseAll( );

  trap_LAN_LoadCachedServers();

  // sets defaults for ui temp cvars
  trap_Cvar_Set( "ui_mousePitch", ( trap_Cvar_VariableValue( "m_pitch" ) >= 0 ) ? "0" : "1" );

  uiInfo.serverStatus.currentServerCinematic = -1;
  uiInfo.previewMovie = -1;

  UI_ParseResolutions( );
}


/*
=================
UI_KeyEvent
=================
*/
void UI_KeyEvent( int key, qboolean down )
{
  if( Menu_Count() > 0 )
  {
    menuDef_t *menu = Menu_GetFocused();

    if( menu )
    {
      if( key == K_ESCAPE && down && !Menus_AnyFullScreenVisible() )
        Menus_CloseAll( );
      else
        Menu_HandleKey( menu, key, down );
    }
    else
    {
      trap_Key_SetCatcher( trap_Key_GetCatcher() & ~KEYCATCH_UI );
      trap_Key_ClearStates();
      trap_Cvar_Set( "cl_paused", "0" );
    }
  }
}

/*
=================
UI_MouseEvent
=================
*/
void UI_MouseEvent( int dx, int dy )
{
  // update mouse screen position
  uiInfo.uiDC.cursorx += ( dx * uiInfo.uiDC.aspectScale );

  if( uiInfo.uiDC.cursorx < 0 )
    uiInfo.uiDC.cursorx = 0;
  else if( uiInfo.uiDC.cursorx > SCREEN_WIDTH )
    uiInfo.uiDC.cursorx = SCREEN_WIDTH;

  uiInfo.uiDC.cursory += dy;

  if( uiInfo.uiDC.cursory < 0 )
    uiInfo.uiDC.cursory = 0;
  else if( uiInfo.uiDC.cursory > SCREEN_HEIGHT )
    uiInfo.uiDC.cursory = SCREEN_HEIGHT;

  if( Menu_Count( ) > 0 )
    Display_MouseMove( NULL, uiInfo.uiDC.cursorx, uiInfo.uiDC.cursory );
}

/*
=================
UI_MousePosition
=================
*/
int UI_MousePosition( void )
{
  return (int)rint( uiInfo.uiDC.cursorx ) |
         (int)rint( uiInfo.uiDC.cursory ) << 16;
}

/*
=================
UI_SetMousePosition
=================
*/
void UI_SetMousePosition( int x, int y )
{
  uiInfo.uiDC.cursorx = x;
  uiInfo.uiDC.cursory = y;

  if( Menu_Count( ) > 0 )
    Display_MouseMove( NULL, uiInfo.uiDC.cursorx, uiInfo.uiDC.cursory );
}

void UI_SetActiveMenu( uiMenuCommand_t menu )
{
  char buf[256];

  // this should be the ONLY way the menu system is brought up
  // enusure minumum menu data is cached

  if( Menu_Count() > 0 )
  {
    vec3_t v;
    v[0] = v[1] = v[2] = 0;

    switch( menu )
    {
      case UIMENU_NONE:
        trap_Key_SetCatcher( trap_Key_GetCatcher() & ~KEYCATCH_UI );
        trap_Key_ClearStates();
        trap_Cvar_Set( "cl_paused", "0" );
        Menus_CloseAll( );

        return;

      case UIMENU_MAIN:
        trap_Cvar_Set( "sv_killserver", "1" );
        trap_Key_SetCatcher( KEYCATCH_UI );
        Menus_CloseAll( );
        Menus_ActivateByName( "main" );
        trap_Cvar_VariableStringBuffer( "com_errorMessage", buf, sizeof( buf ) );

        if( strlen( buf ) )
        {
          if( trap_Cvar_VariableValue( "com_errorCode" ) == ERR_SERVERDISCONNECT )
            Menus_ActivateByName( "drop_popmenu" );
          else
            Menus_ActivateByName( "error_popmenu" );
        }

        return;

      case UIMENU_INGAME:
        trap_Cvar_Set( "cl_paused", "1" );
        trap_Key_SetCatcher( KEYCATCH_UI );
        UI_BuildPlayerList();
        Menus_CloseAll( );
        Menus_ActivateByName( "ingame" );
        return;
    }
  }
}

qboolean UI_IsFullscreen( void )
{
  return Menus_AnyFullScreenVisible( );
}



static connstate_t  lastConnState;
static char     lastLoadingText[MAX_INFO_VALUE];

static void UI_ReadableSize ( char *buf, int bufsize, int value )
{
  if( value > 1024 * 1024 * 1024 )
  { // gigs
    Com_sprintf( buf, bufsize, "%d", value / ( 1024 * 1024 * 1024 ) );
    Com_sprintf( buf + strlen( buf ), bufsize - strlen( buf ), ".%02d GB",
        ( value % ( 1024 * 1024 * 1024 ) ) * 100 / ( 1024 * 1024 * 1024 ) );
  }
  else if( value > 1024 * 1024 )
  { // megs
    Com_sprintf( buf, bufsize, "%d", value / ( 1024 * 1024 ) );
    Com_sprintf( buf + strlen( buf ), bufsize - strlen( buf ), ".%02d MB",
        ( value % ( 1024 * 1024 ) ) * 100 / ( 1024 * 1024 ) );
  }
  else if( value > 1024 )
  { // kilos
    Com_sprintf( buf, bufsize, "%d KB", value / 1024 );
  }
  else
  { // bytes
    Com_sprintf( buf, bufsize, "%d bytes", value );
  }
}

// Assumes time is in msec
static void UI_PrintTime ( char *buf, int bufsize, int time )
{
  time /= 1000;  // change to seconds

  if( time > 3600 )
  { // in the hours range
    Com_sprintf( buf, bufsize, "%d hr %d min", time / 3600, ( time % 3600 ) / 60 );
  }
  else if( time > 60 )
  { // mins
    Com_sprintf( buf, bufsize, "%d min %d sec", time / 60, time % 60 );
  }
  else
  { // secs
    Com_sprintf( buf, bufsize, "%d sec", time );
  }
}

// FIXME: move to ui_shared.c?
void Text_PaintCenter( float x, float y, float scale, vec4_t color, const char *text, float adjust )
{
  int len = UI_Text_Width( text, scale );
  UI_Text_Paint( x - len / 2, y, scale, color, text, 0, 0, ITEM_TEXTSTYLE_SHADOWEDMORE );
}

void Text_PaintCenter_AutoWrapped( float x, float y, float xmax, float ystep, float scale, vec4_t color, const char *str, float adjust )
{
  int width;
  char *s1, *s2, *s3;
  char c_bcp;
  char buf[1024];

  if( !str || str[0] == '\0' )
    return;

  Q_strncpyz( buf, str, sizeof( buf ) );

  s1 = s2 = s3 = buf;

  while( 1 )
  {
    do
      s3++;
    while( *s3 != ' ' && *s3 != '\0' );

    c_bcp = *s3;

    *s3 = '\0';

    width = UI_Text_Width( s1, scale );

    *s3 = c_bcp;

    if( width > xmax )
    {
      if( s1 == s2 )
      {
        // fuck, don't have a clean cut, we'll overflow
        s2 = s3;
      }

      *s2 = '\0';
      Text_PaintCenter( x, y, scale, color, s1, adjust );
      y += ystep;

      if( c_bcp == '\0' )
      {
        // that was the last word
        // we could start a new loop, but that wouldn't be much use
        // even if the word is too long, we would overflow it (see above)
        // so just print it now if needed
        s2++;

        if( *s2 != '\0' ) // if we are printing an overflowing line we have s2 == s3
          Text_PaintCenter( x, y, scale, color, s2, adjust );

        break;
      }

      s2++;
      s1 = s2;
      s3 = s2;
    }
    else
    {
      s2 = s3;

      if( c_bcp == '\0' ) // we reached the end
      {
        Text_PaintCenter( x, y, scale, color, s1, adjust );
        break;
      }
    }
  }
}


static void UI_DisplayDownloadInfo( const char *downloadName, float centerPoint, float yStart, float scale )
{
  static char dlText[]  = "Downloading:";
  static char etaText[] = "Estimated time left:";
  static char xferText[]  = "Transfer rate:";

  int downloadSize, downloadCount, downloadTime;
  char dlSizeBuf[64], totalSizeBuf[64], xferRateBuf[64], dlTimeBuf[64];
  int xferRate;
  int leftWidth;
  const char *s;

  downloadSize = trap_Cvar_VariableValue( "cl_downloadSize" );
  downloadCount = trap_Cvar_VariableValue( "cl_downloadCount" );
  downloadTime = trap_Cvar_VariableValue( "cl_downloadTime" );

  leftWidth = 320;

  UI_SetColor( colorWhite );
  Text_PaintCenter( centerPoint, yStart + 112, scale, colorWhite, dlText, 0 );
  Text_PaintCenter( centerPoint, yStart + 192, scale, colorWhite, etaText, 0 );
  Text_PaintCenter( centerPoint, yStart + 248, scale, colorWhite, xferText, 0 );

  if( downloadSize > 0 )
    s = va( "%s (%d%%)", downloadName, ( int )( ( float )downloadCount * 100.0f / downloadSize ) );
  else
    s = downloadName;

  Text_PaintCenter( centerPoint, yStart + 136, scale, colorWhite, s, 0 );

  UI_ReadableSize( dlSizeBuf,   sizeof dlSizeBuf,   downloadCount );
  UI_ReadableSize( totalSizeBuf,  sizeof totalSizeBuf,  downloadSize );

  if( downloadCount < 4096 || !downloadTime )
  {
    Text_PaintCenter( leftWidth, yStart + 216, scale, colorWhite, "estimating", 0 );
    Text_PaintCenter( leftWidth, yStart + 160, scale, colorWhite, va( "(%s of %s copied)", dlSizeBuf, totalSizeBuf ), 0 );
  }
  else
  {
    if( ( uiInfo.uiDC.realTime - downloadTime ) / 1000 )
      xferRate = downloadCount / ( ( uiInfo.uiDC.realTime - downloadTime ) / 1000 );
    else
      xferRate = 0;

    UI_ReadableSize( xferRateBuf, sizeof xferRateBuf, xferRate );

    // Extrapolate estimated completion time

    if( downloadSize && xferRate )
    {
      int n = downloadSize / xferRate; // estimated time for entire d/l in secs

      // We do it in K (/1024) because we'd overflow around 4MB
      UI_PrintTime ( dlTimeBuf, sizeof dlTimeBuf,
          ( n - ( ( ( downloadCount / 1024 ) * n ) / ( downloadSize / 1024 ) ) ) * 1000 );

      Text_PaintCenter( leftWidth, yStart + 216, scale, colorWhite, dlTimeBuf, 0 );
      Text_PaintCenter( leftWidth, yStart + 160, scale, colorWhite, va( "(%s of %s copied)", dlSizeBuf, totalSizeBuf ), 0 );
    }
    else
    {
      Text_PaintCenter( leftWidth, yStart + 216, scale, colorWhite, "estimating", 0 );

      if( downloadSize )
        Text_PaintCenter( leftWidth, yStart + 160, scale, colorWhite, va( "(%s of %s copied)", dlSizeBuf, totalSizeBuf ), 0 );
      else
        Text_PaintCenter( leftWidth, yStart + 160, scale, colorWhite, va( "(%s copied)", dlSizeBuf ), 0 );
    }

    if( xferRate )
      Text_PaintCenter( leftWidth, yStart + 272, scale, colorWhite, va( "%s/Sec", xferRateBuf ), 0 );
  }
}

/*
========================
UI_DrawConnectScreen
========================
*/
void UI_DrawConnectScreen( qboolean overlay )
{
  char      *s;
  uiClientState_t cstate;
  char      info[MAX_INFO_VALUE];
  char text[256];
  float centerPoint, yStart, scale;

  menuDef_t *menu = Menus_FindByName( "Connect" );


  if( !overlay && menu )
    Menu_Paint( menu, qtrue );

  if( !overlay )
  {
    centerPoint = 320;
    yStart = 130;
    scale = 0.5f;
  }
  else
  {
    centerPoint = 320;
    yStart = 32;
    scale = 0.6f;
    return;
  }

  // see what information we should display
  trap_GetClientState( &cstate );

  info[0] = '\0';

  if( trap_GetConfigString( CS_SERVERINFO, info, sizeof( info ) ) )
    Text_PaintCenter( centerPoint, yStart, scale, colorWhite, va( "Loading %s", Info_ValueForKey( info, "mapname" ) ), 0 );

  if( !Q_stricmp( cstate.servername, "localhost" ) )
    Text_PaintCenter( centerPoint, yStart + 48, scale, colorWhite,
                      "Starting up...", ITEM_TEXTSTYLE_SHADOWEDMORE );
  else
  {
    Com_sprintf( text, sizeof( text ), "Connecting to %s", cstate.servername );
    Text_PaintCenter( centerPoint, yStart + 48, scale, colorWhite, text , ITEM_TEXTSTYLE_SHADOWEDMORE );
  }


  // display global MOTD at bottom
  Text_PaintCenter( centerPoint, 600, scale, colorWhite, Info_ValueForKey( cstate.updateInfoString, "motd" ), 0 );

  // print any server info (server full, bad version, etc)
  if( cstate.connState < CA_CONNECTED )
    Text_PaintCenter( centerPoint, yStart + 176, scale, colorWhite, cstate.messageString, 0 );

  if( lastConnState > cstate.connState )
    lastLoadingText[0] = '\0';

  lastConnState = cstate.connState;

  switch( cstate.connState )
  {
    case CA_CONNECTING:
      s = va( "Awaiting connection...%i", cstate.connectPacketCount );
      break;

    case CA_CHALLENGING:
      s = va( "Awaiting challenge...%i", cstate.connectPacketCount );
      break;

    case CA_CONNECTED:
      {
        char downloadName[MAX_INFO_VALUE];
        int prompt = trap_Cvar_VariableValue( "com_downloadPrompt" );
        
        if( prompt & DLP_SHOW ) {
          Com_Printf("Opening download prompt...\n");
          trap_Key_SetCatcher( KEYCATCH_UI );
          Menus_ActivateByName("download_popmenu");
          trap_Cvar_Set( "com_downloadPrompt", "0" );
        }
        
        trap_Cvar_VariableStringBuffer( "cl_downloadName", downloadName, sizeof( downloadName ) );

        if( *downloadName )
        {
          UI_DisplayDownloadInfo( downloadName, centerPoint, yStart, scale );
          return;
        }
      }

      s = "Awaiting gamestate...";
      break;

    case CA_LOADING:
      return;

    case CA_PRIMED:
      return;

    default:
      return;
  }


  if( Q_stricmp( cstate.servername, "localhost" ) )
    Text_PaintCenter( centerPoint, yStart + 80, scale, colorWhite, s, 0 );

  // password required / connection rejected information goes here
}

/*
=================
UI_RegisterCvars
=================
*/
void UI_RegisterCvars( void )
{
  int     i;
  cvarTable_t *cv;

  for( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ )
    trap_Cvar_Register( cv->vmCvar, cv->cvarName, cv->defaultString, cv->cvarFlags );
}

/*
=================
UI_UpdateCvars
=================
*/
void UI_UpdateCvars( void )
{
  int     i;
  cvarTable_t *cv;

  for( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ )
    trap_Cvar_Update( cv->vmCvar );
}

/*
=================
UI_UpdateNews
=================
*/
void UI_UpdateNews( qboolean begin )
{
  char newsString[ MAX_NEWS_STRING ];
  const char *c;
  const char *wrapped;
  int line = 0;
  int linePos = 0;
  qboolean finished;

  if( begin && !uiInfo.newsInfo.refreshActive ) {
    uiInfo.newsInfo.refreshtime = uiInfo.uiDC.realTime + 10000;
    uiInfo.newsInfo.refreshActive = qtrue;
  }
  else if( !uiInfo.newsInfo.refreshActive ) // do nothing
    return; 
  else if( uiInfo.uiDC.realTime > uiInfo.newsInfo.refreshtime ) {
    strcpy( uiInfo.newsInfo.text[ 0 ], 
      "^1Error: Timed out while contacting the server.");
    uiInfo.newsInfo.numLines = 1;
    return; 
  }

  // start the news fetching
  finished = trap_GetNews( begin );

  // parse what comes back. Parse newlines and otherwise chop when necessary
  trap_Cvar_VariableStringBuffer( "cl_newsString", newsString, 
    sizeof( newsString ) );

  // FIXME remove magic width constant
  wrapped = Item_Text_Wrap( newsString, 0.25f, 325 * uiInfo.uiDC.aspectScale );

  for( c = wrapped; *c != '\0'; ++c ) {
    if( linePos == (MAX_NEWS_LINEWIDTH - 1) || *c == '\n' ) {
      uiInfo.newsInfo.text[ line ][ linePos ] = '\0';

      if( line == ( MAX_NEWS_LINES  - 1 ) )
        break;

      linePos = 0;
      line++;

      if( *c != '\n' ) {
        uiInfo.newsInfo.text[ line ][ linePos ] = *c;
        linePos++;
      }
    } else if( isprint( *c ) ) {
      uiInfo.newsInfo.text[ line ][ linePos ] = *c;
      linePos++;
    }
  }

  uiInfo.newsInfo.text[ line ] [linePos ] = '\0';
  uiInfo.newsInfo.numLines = line + 1;

  if( finished )
    uiInfo.newsInfo.refreshActive = qfalse;
}

