Merge branch 'master' into TimePath/experiments/csqc_prediction
authorTimePath <andrew.hardaker1995@gmail.com>
Mon, 2 Feb 2015 08:53:04 +0000 (19:53 +1100)
committerTimePath <andrew.hardaker1995@gmail.com>
Mon, 2 Feb 2015 08:59:48 +0000 (19:59 +1100)
Conflicts:
qcsrc/menu/classes.c
qcsrc/menu/item/modalcontroller.qc
qcsrc/menu/menu.qh
qcsrc/menu/xonotic/maplist.qc
qcsrc/server/command/banning.qc
qcsrc/server/ipban.qc
qcsrc/server/miscfunctions.qc

38 files changed:
1  2 
defaultXonotic.cfg
qcsrc/common/playerstats.qc
qcsrc/common/weapons/weapons.qc
qcsrc/common/weapons/weapons.qh
qcsrc/dpdefs/progsdefs.qh
qcsrc/menu/command/menu_cmd.qc
qcsrc/menu/item.qc
qcsrc/menu/item/button.qc
qcsrc/menu/item/checkbox.qc
qcsrc/menu/item/dialog.qc
qcsrc/menu/item/inputbox.qc
qcsrc/menu/item/listbox.qc
qcsrc/menu/item/modalcontroller.qc
qcsrc/menu/item/nexposee.qc
qcsrc/menu/item/slider.qc
qcsrc/menu/menu.qc
qcsrc/menu/menu.qh
qcsrc/menu/xonotic/colorpicker.qc
qcsrc/menu/xonotic/colorpicker_string.qc
qcsrc/menu/xonotic/dialog_multiplayer_profile.qc
qcsrc/menu/xonotic/dialog_settings_audio.qc
qcsrc/menu/xonotic/dialog_settings_game_crosshair.qc
qcsrc/menu/xonotic/dialog_singleplayer_winner.qc
qcsrc/menu/xonotic/gametypelist.qc
qcsrc/menu/xonotic/keybinder.qc
qcsrc/menu/xonotic/languagelist.qc
qcsrc/menu/xonotic/maplist.qc
qcsrc/menu/xonotic/playerlist.qc
qcsrc/menu/xonotic/serverlist.qc
qcsrc/menu/xonotic/skinlist.qc
qcsrc/server/cl_client.qc
qcsrc/server/command/banning.qc
qcsrc/server/command/common.qc
qcsrc/server/command/common.qh
qcsrc/server/command/vote.qc
qcsrc/server/ipban.qc
qcsrc/server/miscfunctions.qh
qcsrc/server/t_items.qc

Simple merge
Simple merge
Simple merge
@@@ -69,6 -66,6 +69,7 @@@ WepSet WEPSET_SUPERWEAPONS
  // functions:
  entity get_weaponinfo(float id);
  string W_FixWeaponOrder(string order, float complete);
++string W_UndeprecateName(string s);
  string W_NameWeaponOrder(string order);
  string W_NumberWeaponOrder(string order);
  string W_FixWeaponOrder_BuildImpulseList(string o);
index 30ebe1b,0000000..fe632ce
mode 100644,000000..100644
--- /dev/null
@@@ -1,508 -1,0 +1,511 @@@
 +#ifndef PROGSDEFS_H
 +#define PROGSDEFS_H
 +
 +/*
 +==============================================================================
 +
 +                      SOURCE FOR GLOBALVARS_T C STRUCTURE
 +                      MUST NOT BE MODIFIED, OR CRC ERRORS WILL APPEAR
 +
 +==============================================================================
 +*/
 +
 +//
 +// system globals
 +//
 +entity                self;
 +entity                other;
 +entity                world;
 +float         time;
 +float         frametime;
 +
 +float         force_retouch;          // force all entities to touch triggers
 +                                                              // next frame.  this is needed because
 +                                                              // non-moving things don't normally scan
 +                                                              // for triggers, and when a trigger is
 +                                                              // created (like a teleport trigger), it
 +                                                              // needs to catch everything.
 +                                                              // decremented each frame, so set to 2
 +                                                              // to guarantee everything is touched
 +string                mapname;
 +
 +float         deathmatch;
 +float         coop;
 +float         teamplay;
 +
 +int                   serverflags;            // propagated from level to level, used to
 +                                                              // keep track of completed episodes
 +
 +float         total_secrets;
 +float         total_monsters;
 +
 +float         found_secrets;          // number of secrets found
 +float         killed_monsters;        // number of monsters killed
 +
 +
 +// spawnparms are used to encode information about clients across server
 +// level changes
 +float         parm1, parm2, parm3, parm4, parm5, parm6, parm7, parm8, parm9, parm10, parm11, parm12, parm13, parm14, parm15, parm16;
 +
 +//
 +// global variables set by built in functions
 +//
 +vector                v_forward, v_up, v_right;       // set by makevectors()
 +
 +// set by traceline / tracebox
 +float         trace_allsolid;
 +float         trace_startsolid;
 +float         trace_fraction;
 +vector                trace_endpos;
 +vector                trace_plane_normal;
 +float         trace_plane_dist;
 +entity                trace_ent;
 +float         trace_inopen;
 +float         trace_inwater;
 +
 +entity                msg_entity;                             // destination of single entity writes
 +
 +//
 +// required prog functions
 +//
 +void()                main;                                           // only for testing
 +
 +void()                StartFrame;
 +
 +void()                PlayerPreThink;
 +void()                PlayerPostThink;
 +
 +void()                ClientKill;
++#ifdef DP_EXT_PRECONNECT
++void()                ClientPreConnect;
++#endif
 +void()                ClientConnect;
 +void()                PutClientInServer;              // call after setting the parm1... parms
 +void()                ClientDisconnect;
 +
 +void()                SetNewParms;                    // called when a client first connects to
 +                                                                      // a server. sets parms so they can be
 +                                                                      // saved off for restarts
 +
 +void()                SetChangeParms;                 // call to set parms for self so they can
 +                                                                      // be saved for a level transition
 +
 +
 +//================================================
 +void          end_sys_globals;                // flag for structure dumping
 +//================================================
 +
 +/*
 +==============================================================================
 +
 +                      SOURCE FOR ENTVARS_T C STRUCTURE
 +                      MUST NOT BE MODIFIED, OR CRC ERRORS WILL APPEAR
 +
 +==============================================================================
 +*/
 +
 +//
 +// system fields (*** = do not set in prog code, maintained by C code)
 +//
 +.int          modelindex;             // *** model index in the precached list
 +.vector               absmin, absmax; // *** origin + mins / maxs
 +
 +.float                ltime;                  // local time for entity
 +.float                movetype;
 +.float                solid;
 +
 +.vector               origin;                 // ***
 +.vector               oldorigin;              // ***
 +.vector               velocity;
 +.vector               angles;
 +.vector               avelocity;
 +
 +.vector               punchangle;             // temp angle adjust from damage or recoil
 +
 +.string               classname;              // spawn function
 +.string               model;
 +.int          frame;
 +.int          skin;
 +.int          effects;
 +
 +.vector               mins, maxs;             // bounding box extents reletive to origin
 +.vector               size;                   // maxs - mins
 +
 +.void()               touch;
 +.void()               use;
 +.void()               think;
 +.void()               blocked;                // for doors or plats, called when can't push other
 +
 +.float                nextthink;
 +.entity               groundentity;
 +
 +// stats
 +.float                health;
 +.float                frags;
 +.int          weapon;                 // one of the IT_SHOTGUN, etc flags
 +.string               weaponmodel;
 +.float                weaponframe;
 +.float                currentammo;
 +.float                ammo_shells, ammo_nails, ammo_rockets, ammo_cells;
 +
 +.int          items;                  // bit flags
 +
 +.float                takedamage;
 +.entity               chain;
 +.float                deadflag;
 +
 +.vector               view_ofs;                       // add to origin to get eye point
 +
 +
 +.float                button0;                // fire
 +.float                button1;                // use
 +.float                button2;                // jump
 +
 +.float                impulse;                // weapon changes
 +
 +.float                fixangle;
 +.vector               v_angle;                // view / targeting angle for players
 +.float                idealpitch;             // calculated pitch angle for lookup up slopes
 +
 +
 +.string               netname;
 +
 +.entity       enemy;
 +
 +.int          flags;
 +
 +.int          colormap;
 +.float                team;
 +
 +.float                max_health;             // players maximum health is stored here
 +
 +.float                teleport_time;  // don't back up
 +
 +.float                armortype;              // save this fraction of incoming damage
 +.float                armorvalue;
 +
 +.float                waterlevel;             // 0 = not in, 1 = feet, 2 = wast, 3 = eyes
 +.float                watertype;              // a contents value
 +
 +.float                ideal_yaw;
 +.float                yaw_speed;
 +
 +.entity               aiment;
 +
 +.entity       goalentity;             // a movetarget or an enemy
 +
 +.int          spawnflags;
 +
 +.string               target;
 +.string               targetname;
 +
 +// damage is accumulated through a frame. and sent as one single
 +// message, so the super shotgun doesn't generate huge messages
 +.float                dmg_take;
 +.float                dmg_save;
 +.entity               dmg_inflictor;
 +
 +.entity               owner;          // who launched a missile
 +.vector               movedir;        // mostly for doors, but also used for waterjump
 +
 +.string               message;                // trigger messages
 +
 +.float                sounds;         // either a cd track number or sound number
 +
 +.string               noise, noise1, noise2, noise3;  // contains names of wavs to play
 +
 +//================================================
 +void          end_sys_fields;                 // flag for structure dumping
 +//================================================
 +
 +/*
 +==============================================================================
 +
 +                              CONSTANT DEFINITIONS
 +
 +==============================================================================
 +*/
 +
 +
 +//
 +// constants
 +//
 +
 +// edict.flags
 +const int FL_FLY                              = 1;
 +const int FL_SWIM                             = 2;
 +const int FL_CLIENT                           = 8;    // set for all client edicts
 +const int FL_INWATER                  = 16;   // for enter / leave water splash
 +const int FL_MONSTER                  = 32;
 +const int FL_GODMODE                  = 64;   // player cheat
 +const int FL_NOTARGET                 = 128;  // player cheat
 +const int FL_ITEM                             = 256;  // extra wide size for bonus items
 +const int FL_ONGROUND                 = 512;  // standing on something
 +const int FL_PARTIALGROUND            = 1024; // not all corners are valid
 +const int FL_WATERJUMP                        = 2048; // player jumping out of water
 +const int FL_JUMPRELEASED             = 4096; // for jump debouncing
 +
 +// edict.movetype values
 +const int MOVETYPE_NONE                       = 0;    // never moves
 +//const int   MOVETYPE_ANGLENOCLIP= 1;
 +//const int   MOVETYPE_ANGLECLIP      = 2;
 +const int MOVETYPE_WALK                       = 3;    // players only
 +const int MOVETYPE_STEP                       = 4;    // discrete, not real time unless fall
 +const int MOVETYPE_FLY                        = 5;
 +const int MOVETYPE_TOSS                       = 6;    // gravity
 +const int MOVETYPE_PUSH                       = 7;    // no clip to world, push and crush
 +const int MOVETYPE_NOCLIP             = 8;
 +const int MOVETYPE_FLYMISSILE = 9;    // fly with extra size against monsters
 +const int MOVETYPE_BOUNCE             = 10;
 +const int MOVETYPE_BOUNCEMISSILE= 11; // bounce with extra size
 +
 +// edict.solid values
 +const int SOLID_NOT                           = 0;    // no interaction with other objects
 +const int SOLID_TRIGGER                       = 1;    // touch on edge, but not blocking
 +const int SOLID_BBOX                  = 2;    // touch on edge, block
 +const int SOLID_SLIDEBOX              = 3;    // touch on edge, but not an onground
 +const int SOLID_BSP                           = 4;    // bsp clip, touch on edge, block
 +
 +// range values
 +const int RANGE_MELEE                 = 0;
 +const int RANGE_NEAR                  = 1;
 +const int RANGE_MID                           = 2;
 +const int RANGE_FAR                           = 3;
 +
 +// deadflag values
 +
 +const int DEAD_NO                             = 0;
 +const int DEAD_DYING                  = 1;
 +const int DEAD_DEAD                           = 2;
 +const int DEAD_RESPAWNABLE            = 3;
 +const int DEAD_RESPAWNING             = 4; // dead, waiting for buttons to be released
 +
 +// takedamage values
 +
 +const int DAMAGE_NO                           = 0;
 +const int DAMAGE_YES                  = 1;
 +const int DAMAGE_AIM                  = 2;
 +
 +// items
 +const int IT_AXE                              = 4096;
 +const int IT_SHOTGUN                  = 1;
 +const int IT_SUPER_SHOTGUN            = 2;
 +const int IT_NAILGUN                  = 4;
 +const int IT_SUPER_NAILGUN            = 8;
 +const int IT_GRENADE_LAUNCHER = 16;
 +const int IT_ROCKET_LAUNCHER  = 32;
 +const int IT_LIGHTNING                        = 64;
 +const int IT_EXTRA_WEAPON             = 128;
 +
 +//const int IT_SHELLS                 = 256;
 +//const int IT_NAILS                  = 512;
 +//const int IT_ROCKETS                        = 1024;
 +//const int IT_CELLS                  = 2048;
 +
 +const int IT_ARMOR1                           = 8192;
 +const int IT_ARMOR2                           = 16384;
 +const int IT_ARMOR3                           = 32768;
 +const int IT_SUPERHEALTH              = 65536;
 +
 +//const int IT_KEY1                           = 131072;
 +//const int IT_KEY2                           = 262144;
 +
 +const int IT_INVISIBILITY             = 524288;
 +const int IT_INVULNERABILITY  = 1048576;
 +const int IT_SUIT                             = 2097152;
 +const int IT_QUAD                             = 4194304;
 +
 +// point content values
 +
 +const int CONTENT_EMPTY                       = -1;
 +const int CONTENT_SOLID                       = -2;
 +const int CONTENT_WATER                       = -3;
 +const int CONTENT_SLIME                       = -4;
 +const int CONTENT_LAVA                        = -5;
 +const int CONTENT_SKY                 = -6;
 +
 +const int STATE_TOP                           = 0;
 +const int STATE_BOTTOM                        = 1;
 +const int STATE_UP                            = 2;
 +const int STATE_DOWN                  = 3;
 +
 +const vector VEC_ORIGIN               = '0 0 0';
 +const vector VEC_HULL_MIN             = '-16 -16 -24';
 +const vector VEC_HULL_MAX             = '16 16 32';
 +
 +const vector VEC_HULL2_MIN            = '-32 -32 -24';
 +const vector VEC_HULL2_MAX            = '32 32 64';
 +
 +// protocol bytes
 +const int SVC_TEMPENTITY              = 23;
 +const int SVC_KILLEDMONSTER           = 27;
 +const int SVC_FOUNDSECRET             = 28;
 +const int SVC_INTERMISSION            = 30;
 +const int SVC_FINALE                  = 31;
 +const int SVC_CDTRACK                 = 32;
 +const int SVC_SELLSCREEN              = 33;
 +
 +
 +const int TE_SPIKE                            = 0;
 +const int TE_SUPERSPIKE                       = 1;
 +const int TE_GUNSHOT                  = 2;
 +const int TE_EXPLOSION                        = 3;
 +const int TE_TAREXPLOSION             = 4;
 +const int TE_LIGHTNING1                       = 5;
 +const int TE_LIGHTNING2                       = 6;
 +const int TE_WIZSPIKE                 = 7;
 +const int TE_KNIGHTSPIKE              = 8;
 +const int TE_LIGHTNING3                       = 9;
 +const int TE_LAVASPLASH                       = 10;
 +const int TE_TELEPORT                 = 11;
 +
 +// sound channels
 +// channel 0 never willingly overrides
 +// other channels (1-7) allways override a playing sound on that channel
 +const int CHAN_AUTO                           = 0;
 +const int CHAN_WEAPON                 = 1;
 +const int CHAN_VOICE                  = 2;
 +const int CHAN_ITEM                           = 3;
 +const int CHAN_BODY                           = 4;
 +
 +const int ATTN_NONE                           = 0;
 +const int ATTN_NORM                           = 1;
 +const int ATTN_IDLE                           = 2;
 +const int ATTN_STATIC                 = 3;
 +
 +// update types
 +
 +const int UPDATE_GENERAL              = 0;
 +const int UPDATE_STATIC                       = 1;
 +const int UPDATE_BINARY                       = 2;
 +const int UPDATE_TEMP                 = 3;
 +
 +// entity effects
 +
 +const int EF_BRIGHTFIELD              = 1;
 +const int EF_MUZZLEFLASH              = 2;
 +const int EF_BRIGHTLIGHT              = 4;
 +const int EF_DIMLIGHT                         = 8;
 +
 +
 +// messages
 +const int MSG_BROADCAST                       = 0;            // unreliable to all
 +const int MSG_ONE                             = 1;            // reliable to one (msg_entity)
 +const int MSG_ALL                             = 2;            // reliable to all
 +const int MSG_INIT                            = 3;            // write to the init string
 +
 +//===========================================================================
 +
 +//
 +// builtin functions
 +//
 +
 +void(vector ang)      makevectors             = #1;           // sets v_forward, etc globals
 +void(entity e, vector o) setorigin    = #2;
 +void(entity e, string m) setmodel     = #3;           // set movetype and solid first
 +void(entity e, vector min, vector max) setsize = #4;
 +// #5 was removed
 +void() break_to_debugger                                              = #6;
 +float() random                                                = #7;           // returns 0 - 1
 +void(entity e, float chan, string samp, float vol, float atten) sound = #8;
 +vector(vector v) normalize                    = #9;
 +void(string e, ...) error                             = #10;
 +void(string e, ...) objerror                          = #11;
 +float(vector v) vlen                          = #12;
 +float(vector v) vectoyaw                      = #13;
 +entity() spawn                                                = #14;
 +void(entity e) remove                         = #15;
 +
 +// sets trace_* globals
 +// nomonsters can be:
 +// An entity will also be ignored for testing if forent == test,
 +// forent->owner == test, or test->owner == forent
 +// a forent of world is ignored
 +void(vector v1, vector v2, float nomonsters, entity forent) traceline = #16;
 +
 +entity() checkclient                          = #17;  // returns a client to look for
 +entity(entity start, .string fld, string match) find = #18;
 +string(string s) precache_sound               = #19;
 +string(string s) precache_model               = #20;
 +void(entity client, string s, ...)stuffcmd = #21;
 +entity(vector org, float rad) findradius = #22;
 +void(string s, ...) bprint                            = #23;
 +void(entity client, string s, ...) sprint = #24;
 +void(string s, ...) dprint                            = #25;
 +string(float f) ftos                          = #26;
 +string(vector v) vtos                         = #27;
 +void() coredump                                               = #28;          // prints all edicts
 +void() traceon                                                = #29;          // turns statment trace on
 +void() traceoff                                               = #30;
 +void(entity e) eprint                         = #31;          // prints an entire edict
 +float(float yaw, float dist) walkmove = #32;  // returns true or false
 +// #33 was removed
 +float() droptofloor= #34;     // true if landed on floor
 +void(float style, string value) lightstyle = #35;
 +float(float v) rint                                   = #36;          // round to nearest int
 +float(float v) floor                          = #37;          // largest integer <= v
 +float(float v) ceil                                   = #38;          // smallest integer >= v
 +// #39 was removed
 +float(entity e) checkbottom                   = #40;          // true if self is on ground
 +float(vector v) pointcontents         = #41;          // returns a CONTENT_*
 +// #42 was removed
 +float(float f) fabs = #43;
 +vector(entity e, float speed) aim = #44;              // returns the shooting vector
 +float(string s) cvar = #45;                                           // return cvar.value
 +void(string s, ...) localcmd = #46;                                   // put string into local que
 +entity(entity e) nextent = #47;                                       // for looping through all ents
 +void(vector o, vector d, float color, float count) particle = #48;// start a particle effect
 +void() ChangeYaw = #49;                                               // turn towards self.ideal_yaw
 +                                                                                      // at self.yaw_speed
 +// #50 was removed
 +vector(vector v) vectoangles                  = #51;
 +
 +//
 +// direct client message generation
 +//
 +void(float to, float f) WriteByte             = #52;
 +void(float to, float f) WriteChar             = #53;
 +void(float to, float f) WriteShort            = #54;
 +void(float to, float f) WriteLong             = #55;
 +void(float to, float f) WriteCoord            = #56;
 +void(float to, float f) WriteAngle            = #57;
 +void(float to, string s, ...) WriteString     = #58;
 +void(float to, entity s) WriteEntity  = #59;
 +
 +//
 +// broadcast client message generation
 +//
 +
 +// void(float f) bWriteByte           = #59;
 +// void(float f) bWriteChar           = #60;
 +// void(float f) bWriteShort          = #61;
 +// void(float f) bWriteLong           = #62;
 +// void(float f) bWriteCoord          = #63;
 +// void(float f) bWriteAngle          = #64;
 +// void(string s) bWriteString        = #65;
 +// void(entity e) bWriteEntity = #66;
 +
 +void(float step) movetogoal                           = #67;
 +
 +string(string s) precache_file                = #68;  // no effect except for -copy
 +void(entity e) makestatic             = #69;
 +void(string s) changelevel = #70;
 +
 +//#71 was removed
 +
 +void(string name, string value) cvar_set = #72;       // sets cvar.value
 +
 +void(entity client, string s, ...) centerprint = #73; // sprint, but in middle
 +
 +void(vector pos, string samp, float vol, float atten) ambientsound = #74;
 +
 +string(string s) precache_model2      = #75;          // registered version only
 +string(string s) precache_sound2      = #76;          // registered version only
 +string(string s) precache_file2               = #77;          // registered version only
 +
 +void(entity e) setspawnparms          = #78;          // set parm1... to the
 +                                                                                              // values at level start
 +                                                                                              // for coop respawn
 +
 +//============================================================================
 +#endif
Simple merge
index d055b1a,0000000..d0bd40b
mode 100644,000000..100644
--- /dev/null
@@@ -1,134 -1,0 +1,137 @@@
 +#ifdef INTERFACE
 +CLASS(Item) EXTENDS(Object)
 +      METHOD(Item, draw, void(entity))
 +      METHOD(Item, keyDown, float(entity, float, float, float))
 +      METHOD(Item, keyUp, float(entity, float, float, float))
 +      METHOD(Item, mouseMove, float(entity, vector))
 +      METHOD(Item, mousePress, float(entity, vector))
 +      METHOD(Item, mouseDrag, float(entity, vector))
 +      METHOD(Item, mouseRelease, float(entity, vector))
 +      METHOD(Item, focusEnter, void(entity))
 +      METHOD(Item, focusLeave, void(entity))
 +      METHOD(Item, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(Item, relinquishFocus, void(entity))
 +      METHOD(Item, showNotify, void(entity))
 +      METHOD(Item, hideNotify, void(entity))
 +      METHOD(Item, toString, string(entity))
 +      METHOD(Item, destroy, void(entity))
 +      ATTRIB(Item, focused, float, 0)
 +      ATTRIB(Item, focusable, float, 0)
++      ATTRIB(Item, allowFocusSound, float, 0)
 +      ATTRIB(Item, parent, entity, NULL)
 +      ATTRIB(Item, preferredFocusPriority, float, 0)
 +      ATTRIB(Item, origin, vector, '0 0 0')
 +      ATTRIB(Item, size, vector, '0 0 0')
 +      ATTRIB(Item, tooltip, string, string_null)
 +ENDCLASS(Item)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void Item_destroy(entity me)
 +{
 +      // free memory associated with me
 +}
 +
 +void Item_relinquishFocus(entity me)
 +{
 +      if(me.parent)
 +              if(me.parent.instanceOfContainer)
 +                      me.parent.setFocus(me.parent, NULL);
 +}
 +
 +void Item_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.origin = absOrigin;
 +      me.size = absSize;
 +}
 +
 +float autocvar_menu_showboxes;
 +void Item_draw(entity me)
 +{
 +      if(autocvar_menu_showboxes)
 +      {
 +              vector rgb = '1 0 1';
 +              float a = fabs(autocvar_menu_showboxes);
 +
 +              // don't draw containers and border images
 +              if(me.instanceOfContainer || me.instanceOfBorderImage)
 +              {
 +                      rgb = '0 0 0';
 +                      a = 0;
 +              }
 +
 +#if 0
 +              // hack to detect multi drawing
 +              float r = random() * 3;
 +              if(r >= 2)
 +                      rgb = '1 0 0';
 +              else if(r >= 1)
 +                      rgb = '0 1 0';
 +              else
 +                      rgb = '0 0 1';
 +#endif
 +              if(autocvar_menu_showboxes < 0)
 +              {
 +                      draw_Fill('0 0 0', '0.5 0.5 0', rgb, a);
 +                      draw_Fill('0.5 0.5 0', '0.5 0.5 0', rgb, a);
 +              }
 +              if(autocvar_menu_showboxes > 0)
 +              {
 +                      draw_Fill('0 0 0', '1 1 0', rgb, a);
 +              }
 +      }
 +}
 +
 +void Item_showNotify(entity me)
 +{
 +}
 +
 +void Item_hideNotify(entity me)
 +{
 +}
 +
 +float Item_keyDown(entity me, float scan, float ascii, float shift)
 +{
 +      return 0; // unhandled
 +}
 +
 +float Item_keyUp(entity me, float scan, float ascii, float shift)
 +{
 +      return 0; // unhandled
 +}
 +
 +float Item_mouseMove(entity me, vector pos)
 +{
 +      return 0; // unhandled
 +}
 +
 +float Item_mousePress(entity me, vector pos)
 +{
 +      return 0; // unhandled
 +}
 +
 +float Item_mouseDrag(entity me, vector pos)
 +{
 +      return 0; // unhandled
 +}
 +
 +float Item_mouseRelease(entity me, vector pos)
 +{
 +      return 0; // unhandled
 +}
 +
 +void Item_focusEnter(entity me)
 +{
++      if(me.allowFocusSound)
++              m_play_focus_sound();
 +}
 +
 +void Item_focusLeave(entity me)
 +{
 +}
 +
 +string Item_toString(entity me)
 +{
 +      return string_null;
 +}
 +#endif
index f6ba208,0000000..52e5823
mode 100644,000000..100644
--- /dev/null
@@@ -1,173 -1,0 +1,178 @@@
-       METHOD(Button, focusEnter, void(entity))
 +#ifdef INTERFACE
 +CLASS(Button) EXTENDS(Label)
 +      METHOD(Button, configureButton, void(entity, string, float, string))
 +      METHOD(Button, draw, void(entity))
 +      METHOD(Button, showNotify, void(entity))
 +      METHOD(Button, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(Button, keyDown, float(entity, float, float, float))
 +      METHOD(Button, mousePress, float(entity, vector))
 +      METHOD(Button, mouseDrag, float(entity, vector))
 +      METHOD(Button, mouseRelease, float(entity, vector))
-                       if(cvar("menu_sounds"))
-                               localsound("sound/misc/menu2.wav");
++      METHOD(Button, playClickSound, void(entity))
 +      ATTRIB(Button, onClick, void(entity, entity), func_null)
 +      ATTRIB(Button, onClickEntity, entity, NULL)
 +      ATTRIB(Button, src, string, string_null)
 +      ATTRIB(Button, srcSuffix, string, string_null)
 +      ATTRIB(Button, src2, string, string_null) // is centered, same aspect, and stretched to label size
 +      ATTRIB(Button, src2scale, float, 1)
 +      ATTRIB(Button, srcMulti, float, 1) // 0: button square left, text right; 1: button stretched, text over it
 +      ATTRIB(Button, buttonLeftOfText, float, 0)
 +      ATTRIB(Button, focusable, float, 1)
++      ATTRIB(Button, allowFocusSound, float, 1)
 +      ATTRIB(Button, pressed, float, 0)
 +      ATTRIB(Button, clickTime, float, 0)
 +      ATTRIB(Button, disabled, float, 0)
 +      ATTRIB(Button, disabledAlpha, float, 0.3)
 +      ATTRIB(Button, forcePressed, float, 0)
 +      ATTRIB(Button, color, vector, '1 1 1')
 +      ATTRIB(Button, colorC, vector, '1 1 1')
 +      ATTRIB(Button, colorF, vector, '1 1 1')
 +      ATTRIB(Button, colorD, vector, '1 1 1')
 +      ATTRIB(Button, color2, vector, '1 1 1')
 +      ATTRIB(Button, alpha2, float, 1)
 +
 +      ATTRIB(Button, origin, vector, '0 0 0')
 +      ATTRIB(Button, size, vector, '0 0 0')
 +ENDCLASS(Button)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void Button_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      if(me.srcMulti)
 +              me.keepspaceLeft = 0;
 +      else
 +              me.keepspaceLeft = min(0.8, absSize.y / absSize.x);
 +      SUPER(Button).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +}
 +void Button_configureButton(entity me, string txt, float sz, string gfx)
 +{
 +      SUPER(Button).configureLabel(me, txt, sz, me.srcMulti ? 0.5 : 0);
 +      me.src = gfx;
 +}
 +float Button_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      if(key == K_ENTER || key == K_SPACE || key == K_KP_ENTER)
 +      {
++              me.playClickSound(me);
 +              me.clickTime = 0.1; // delayed for effect
 +              return 1;
 +      }
 +      return 0;
 +}
 +float Button_mouseDrag(entity me, vector pos)
 +{
 +      me.pressed = 1;
 +      if(pos.x < 0) me.pressed = 0;
 +      if(pos.y < 0) me.pressed = 0;
 +      if(pos.x >= 1) me.pressed = 0;
 +      if(pos.y >= 1) me.pressed = 0;
 +      return 1;
 +}
 +float Button_mousePress(entity me, vector pos)
 +{
 +      me.mouseDrag(me, pos); // verify coordinates
 +      return 1;
 +}
 +float Button_mouseRelease(entity me, vector pos)
 +{
 +      me.mouseDrag(me, pos); // verify coordinates
 +      if(me.pressed)
 +      {
 +              if (!me.disabled)
 +              {
- void Button_focusEnter(entity me)
- {
-       if(cvar("menu_sounds") > 1)
-               localsound("sound/misc/menu1.wav");
-       SUPER(Button).focusEnter(me);
- }
++                      me.playClickSound(me);
 +                      if(me.onClick)
 +                              me.onClick(me, me.onClickEntity);
 +              }
 +              me.pressed = 0;
 +      }
 +      return 1;
 +}
 +void Button_showNotify(entity me)
 +{
 +      me.focusable = !me.disabled;
 +}
 +void Button_draw(entity me)
 +{
 +      vector bOrigin, bSize;
 +      float save;
 +
 +      me.focusable = !me.disabled;
 +
 +      save = draw_alpha;
 +      if(me.disabled)
 +              draw_alpha *= me.disabledAlpha;
 +
 +      if(me.src)
 +      {
 +              if(me.srcMulti)
 +              {
 +                      bOrigin = '0 0 0';
 +                      bSize = '1 1 0';
 +                      if(me.disabled)
 +                              draw_ButtonPicture(bOrigin, strcat(me.src, "_d", me.srcSuffix), bSize, me.colorD, 1);
 +                      else if(me.forcePressed || me.pressed || me.clickTime > 0)
 +                              draw_ButtonPicture(bOrigin, strcat(me.src, "_c", me.srcSuffix), bSize, me.colorC, 1);
 +                      else if(me.focused)
 +                              draw_ButtonPicture(bOrigin, strcat(me.src, "_f", me.srcSuffix), bSize, me.colorF, 1);
 +                      else
 +                              draw_ButtonPicture(bOrigin, strcat(me.src, "_n", me.srcSuffix), bSize, me.color, 1);
 +              }
 +              else
 +              {
 +                      if(me.realFontSize_y == 0)
 +                      {
 +                              bOrigin = '0 0 0';
 +                              bSize = '1 1 0';
 +                      }
 +                      else
 +                      {
 +                              bOrigin = eY * (0.5 * (1 - me.realFontSize.y)) + eX * (0.5 * (me.keepspaceLeft - me.realFontSize.x));
 +                              bSize = me.realFontSize;
 +                      }
 +                      if(me.disabled)
 +                              draw_Picture(bOrigin, strcat(me.src, "_d", me.srcSuffix), bSize, me.colorD, 1);
 +                      else if(me.forcePressed || me.pressed || me.clickTime > 0)
 +                              draw_Picture(bOrigin, strcat(me.src, "_c", me.srcSuffix), bSize, me.colorC, 1);
 +                      else if(me.focused)
 +                              draw_Picture(bOrigin, strcat(me.src, "_f", me.srcSuffix), bSize, me.colorF, 1);
 +                      else
 +                              draw_Picture(bOrigin, strcat(me.src, "_n", me.srcSuffix), bSize, me.color, 1);
 +              }
 +      }
 +      if(me.src2)
 +      {
 +              bOrigin = me.keepspaceLeft * eX;
 +              bSize = eY + eX * (1 - me.keepspaceLeft);
 +
 +              bOrigin += bSize * (0.5 - 0.5 * me.src2scale);
 +              bSize = bSize * me.src2scale;
 +
 +              draw_Picture(bOrigin, me.src2, bSize, me.color2, me.alpha2);
 +      }
 +
 +      draw_alpha = save;
 +
 +      if(me.clickTime > 0 && me.clickTime <= frametime)
 +      {
 +              // keyboard click timer expired? Fire the event then.
 +              if (!me.disabled)
 +                      if(me.onClick)
 +                              me.onClick(me, me.onClickEntity);
 +      }
 +      me.clickTime -= frametime;
 +
 +      SUPER(Button).draw(me);
 +}
++void Dialog_Close(entity button, entity me);
++void Button_playClickSound(entity me)
++{
++      if(me.onClick == DialogOpenButton_Click)
++              m_play_click_sound(MENU_SOUND_OPEN);
++      else if(me.onClick == Dialog_Close)
++              m_play_click_sound(MENU_SOUND_CLOSE);
++      else
++              m_play_click_sound(MENU_SOUND_EXECUTE);
++}
 +#endif
index 94f67ba,0000000..2540cc8
mode 100644,000000..100644
--- /dev/null
@@@ -1,48 -1,0 +1,53 @@@
 +#ifdef INTERFACE
 +void CheckBox_Click(entity me, entity other);
 +CLASS(CheckBox) EXTENDS(Button)
 +      METHOD(CheckBox, configureCheckBox, void(entity, string, float, string))
 +      METHOD(CheckBox, draw, void(entity))
++      METHOD(CheckBox, playClickSound, void(entity))
 +      METHOD(CheckBox, toString, string(entity))
 +      METHOD(CheckBox, setChecked, void(entity, float))
 +      ATTRIB(CheckBox, useDownAsChecked, float, 0)
 +      ATTRIB(CheckBox, checked, float, 0)
 +      ATTRIB(CheckBox, onClick, void(entity, entity), CheckBox_Click)
 +      ATTRIB(CheckBox, srcMulti, float, 0)
 +      ATTRIB(CheckBox, disabled, float, 0)
 +ENDCLASS(CheckBox)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void CheckBox_setChecked(entity me, float val)
 +{
 +      me.checked = val;
 +}
 +void CheckBox_Click(entity me, entity other)
 +{
 +      me.setChecked(me, !me.checked);
 +}
 +string CheckBox_toString(entity me)
 +{
 +      return strcat(SUPER(CheckBox).toString(me), ", ", me.checked ? "checked" : "unchecked");
 +}
 +void CheckBox_configureCheckBox(entity me, string txt, float sz, string gfx)
 +{
 +      me.configureButton(me, txt, sz, gfx);
 +      me.align = 0;
 +}
 +void CheckBox_draw(entity me)
 +{
 +      float s;
 +      s = me.pressed;
 +      if(me.useDownAsChecked)
 +      {
 +              me.srcSuffix = string_null;
 +              me.forcePressed = me.checked;
 +      }
 +      else
 +              me.srcSuffix = (me.checked ? "1" : "0");
 +      me.pressed = s;
 +      SUPER(CheckBox).draw(me);
 +}
++void CheckBox_playClickSound(entity me)
++{
++      m_play_click_sound(MENU_SOUND_SELECT);
++}
 +#endif
index 62c7440,0000000..1723f27
mode 100644,000000..100644
--- /dev/null
@@@ -1,191 -1,0 +1,192 @@@
 +// Note: this class is called Dialog, but it can also handle a tab under the following conditions:
 +// - isTabRoot is 0
 +// - backgroundImage is the tab's background
 +// - closable is 0
 +// - rootDialog is 0
 +// - title is ""
 +// - marginTop is
 +// - intendedHeight ends up to be the tab's actual height, or at least close
 +// - titleFontSize is 0
 +// - marginTop cancels out as much of titleHeight as needed (that is, it should be actualMarginTop - titleHeight)
 +// To ensure the latter, you best create all tabs FIRST and insert the tabbed
 +// control to your dialog THEN - with the right height
 +//
 +// a subclass may help with using this as a tab
 +
 +#ifdef INTERFACE
 +CLASS(Dialog) EXTENDS(InputContainer)
 +      METHOD(Dialog, configureDialog, void(entity)) // no runtime configuration, all parameters are given in the code!
 +      METHOD(Dialog, fill, void(entity)) // to be overridden by user to fill the dialog with controls
 +      METHOD(Dialog, keyDown, float(entity, float, float, float))
 +      METHOD(Dialog, close, void(entity))
 +      METHOD(Dialog, addItemSimple, void(entity, float, float, float, float, entity, vector))
 +
 +      METHOD(Dialog, TD, void(entity, float, float, entity))
 +      METHOD(Dialog, TDNoMargin, void(entity, float, float, entity, vector))
 +      METHOD(Dialog, TDempty, void(entity, float))
 +      METHOD(Dialog, setFirstColumn, void(entity, float))
 +      METHOD(Dialog, TR, void(entity))
 +      METHOD(Dialog, gotoRC, void(entity, float, float))
 +
 +      ATTRIB(Dialog, isTabRoot, float, 1)
 +      ATTRIB(Dialog, closeButton, entity, NULL)
 +      ATTRIB(Dialog, intendedHeight, float, 0)
 +      ATTRIB(Dialog, itemOrigin, vector, '0 0 0')
 +      ATTRIB(Dialog, itemSize, vector, '0 0 0')
 +      ATTRIB(Dialog, itemSpacing, vector, '0 0 0')
 +      ATTRIB(Dialog, currentRow, float, 0)
 +      ATTRIB(Dialog, currentColumn, float, 0)
 +      ATTRIB(Dialog, firstColumn, float, 0)
 +
 +      // to be customized
 +      ATTRIB(Dialog, closable, float, 1)
 +      ATTRIB(Dialog, title, string, "Form1") // ;)
 +      ATTRIB(Dialog, color, vector, '1 0.5 1')
 +      ATTRIB(Dialog, intendedWidth, float, 0)
 +      ATTRIB(Dialog, rows, float, 3)
 +      ATTRIB(Dialog, columns, float, 2)
 +
 +      ATTRIB(Dialog, marginTop, float, 0) // pixels
 +      ATTRIB(Dialog, marginBottom, float, 0) // pixels
 +      ATTRIB(Dialog, marginLeft, float, 0) // pixels
 +      ATTRIB(Dialog, marginRight, float, 0) // pixels
 +      ATTRIB(Dialog, columnSpacing, float, 0) // pixels
 +      ATTRIB(Dialog, rowSpacing, float, 0) // pixels
 +      ATTRIB(Dialog, rowHeight, float, 0) // pixels
 +      ATTRIB(Dialog, titleHeight, float, 0) // pixels
 +      ATTRIB(Dialog, titleFontSize, float, 0) // pixels; if 0, title causes no margin
 +      ATTRIB(Dialog, zoomedOutTitleBarPosition, float, 0)
 +      ATTRIB(Dialog, zoomedOutTitleBar, float, 0)
 +
 +      ATTRIB(Dialog, requiresConnection, float, 0) // set to true if the dialog requires a connection to be opened
 +
 +      ATTRIB(Dialog, backgroundImage, string, string_null)
 +      ATTRIB(Dialog, borderLines, float, 1)
 +      ATTRIB(Dialog, closeButtonImage, string, string_null)
 +
 +      ATTRIB(Dialog, frame, entity, NULL)
 +ENDCLASS(Dialog)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void Dialog_Close(entity button, entity me)
 +{
 +      me.close(me);
 +}
 +
 +void Dialog_fill(entity me)
 +{
 +}
 +
 +void Dialog_addItemSimple(entity me, float row, float col, float rowspan, float colspan, entity e, vector v)
 +{
 +      vector o, s;
 +      o = me.itemOrigin + eX * ( col          * me.itemSpacing.x) + eY * ( row          * me.itemSpacing.y);
 +      s = me.itemSize   + eX * ((colspan - 1) * me.itemSpacing.x) + eY * ((rowspan - 1) * me.itemSpacing.y);
 +      o.x -= 0.5 * (me.itemSpacing.x - me.itemSize.x) * v.x;
 +      s.x +=       (me.itemSpacing.x - me.itemSize.x) * v.x;
 +      o.y -= 0.5 * (me.itemSpacing.y - me.itemSize.y) * v.y;
 +      s.y +=       (me.itemSpacing.y - me.itemSize.y) * v.y;
 +      me.addItem(me, e, o, s, 1);
 +}
 +
 +void Dialog_gotoRC(entity me, float row, float col)
 +{
 +      me.currentRow = row;
 +      me.currentColumn = col;
 +}
 +
 +void Dialog_TR(entity me)
 +{
 +      me.currentRow += 1;
 +      me.currentColumn = me.firstColumn;
 +}
 +
 +void Dialog_TD(entity me, float rowspan, float colspan, entity e)
 +{
 +      me.addItemSimple(me, me.currentRow, me.currentColumn, rowspan, colspan, e, '0 0 0');
 +      me.currentColumn += colspan;
 +}
 +
 +void Dialog_TDNoMargin(entity me, float rowspan, float colspan, entity e, vector v)
 +{
 +      me.addItemSimple(me, me.currentRow, me.currentColumn, rowspan, colspan, e, v);
 +      me.currentColumn += colspan;
 +}
 +
 +void Dialog_setFirstColumn(entity me, float col)
 +{
 +      me.firstColumn = col;
 +}
 +
 +void Dialog_TDempty(entity me, float colspan)
 +{
 +      me.currentColumn += colspan;
 +}
 +
 +void Dialog_configureDialog(entity me)
 +{
 +      float absWidth, absHeight;
 +
 +      me.frame = spawnBorderImage();
 +      me.frame.configureBorderImage(me.frame, me.title, me.titleFontSize, me.color, me.backgroundImage, me.borderLines * me.titleHeight);
 +      me.frame.zoomedOutTitleBarPosition = me.zoomedOutTitleBarPosition;
 +      me.frame.zoomedOutTitleBar = me.zoomedOutTitleBar;
 +      me.frame.alpha = me.alpha;
 +      me.addItem(me, me.frame, '0 0 0', '1 1 0', 1);
 +
 +      if (!me.titleFontSize)
 +              me.titleHeight = 0; // no title bar
 +
 +      absWidth = me.intendedWidth * conwidth;
 +      absHeight = me.borderLines * me.titleHeight + me.marginTop + me.rows * me.rowHeight + (me.rows - 1) * me.rowSpacing + me.marginBottom;
 +      me.itemOrigin  = eX * (me.marginLeft / absWidth)
 +                     + eY * ((me.borderLines * me.titleHeight + me.marginTop) / absHeight);
 +      me.itemSize    = eX * ((1 - (me.marginLeft + me.marginRight + me.columnSpacing * (me.columns - 1)) / absWidth) / me.columns)
 +                     + eY * (me.rowHeight / absHeight);
 +      me.itemSpacing = me.itemSize
 +                     + eX * (me.columnSpacing / absWidth)
 +                     + eY * (me.rowSpacing / absHeight);
 +      me.intendedHeight = absHeight / conheight;
 +      me.currentRow = -1;
 +      me.currentColumn = -1;
 +
 +      me.fill(me);
 +
 +      if(me.closable && me.borderLines > 0)
 +      {
 +              entity closebutton;
 +              closebutton = me.closeButton = me.frame.closeButton = spawnButton();
 +              closebutton.configureButton(closebutton, "", 0, me.closeButtonImage);
 +              closebutton.onClick = Dialog_Close; closebutton.onClickEntity = me;
 +              closebutton.srcMulti = 0;
 +              me.addItem(me, closebutton, '0 0 0', '1 1 0', 1); // put it as LAST
 +      }
 +}
 +
 +void Dialog_close(entity me)
 +{
 +      if(me.parent.instanceOfNexposee)
 +      {
 +              ExposeeCloseButton_Click(me, me.parent);
 +      }
 +      else if(me.parent.instanceOfModalController)
 +      {
 +              DialogCloseButton_Click(me, me);
 +      }
 +}
 +
 +float Dialog_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      if(me.closable)
 +      {
 +              if(key == K_ESCAPE)
 +              {
++                      m_play_click_sound(MENU_SOUND_CLOSE);
 +                      me.close(me);
 +                      return 1;
 +              }
 +      }
 +      return SUPER(Dialog).keyDown(me, key, ascii, shift);
 +}
 +#endif
index d3f6815,0000000..275b200
mode 100644,000000..100644
--- /dev/null
@@@ -1,391 -1,0 +1,390 @@@
- void InputBox_Clear_Click(entity btn, entity me);
 +#ifdef INTERFACE
 +CLASS(InputBox) EXTENDS(Label)
 +      METHOD(InputBox, configureInputBox, void(entity, string, float, float, string))
 +      METHOD(InputBox, draw, void(entity))
 +      METHOD(InputBox, setText, void(entity, string))
 +      METHOD(InputBox, enterText, void(entity, string))
 +      METHOD(InputBox, keyDown, float(entity, float, float, float))
 +      METHOD(InputBox, mouseMove, float(entity, vector))
 +      METHOD(InputBox, mouseRelease, float(entity, vector))
 +      METHOD(InputBox, mousePress, float(entity, vector))
 +      METHOD(InputBox, mouseDrag, float(entity, vector))
 +      METHOD(InputBox, showNotify, void(entity))
 +      METHOD(InputBox, resizeNotify, void(entity, vector, vector, vector, vector))
 +
 +      ATTRIB(InputBox, src, string, string_null)
 +
 +      ATTRIB(InputBox, cursorPos, float, 0) // characters
 +      ATTRIB(InputBox, scrollPos, float, 0) // widths
 +
 +      ATTRIB(InputBox, focusable, float, 1)
++      ATTRIB(InputBox, allowFocusSound, float, 1)
 +      ATTRIB(InputBox, disabled, float, 0)
 +      ATTRIB(InputBox, lastChangeTime, float, 0)
 +      ATTRIB(InputBox, dragScrollTimer, float, 0)
 +      ATTRIB(InputBox, dragScrollPos, vector, '0 0 0')
 +      ATTRIB(InputBox, pressed, float, 0)
 +      ATTRIB(InputBox, editColorCodes, float, 1)
 +      ATTRIB(InputBox, forbiddenCharacters, string, "")
 +      ATTRIB(InputBox, color, vector, '1 1 1')
 +      ATTRIB(InputBox, colorF, vector, '1 1 1')
 +      ATTRIB(InputBox, maxLength, float, 255) // if negative, it counts bytes, not chars
 +
 +      ATTRIB(InputBox, enableClearButton, float, 1)
 +      ATTRIB(InputBox, clearButton, entity, NULL)
 +      ATTRIB(InputBox, cb_width, float, 0)
 +      ATTRIB(InputBox, cb_pressed, float, 0)
 +      ATTRIB(InputBox, cb_focused, float, 0)
 +      ATTRIB(InputBox, cb_color, vector, '1 1 1')
 +      ATTRIB(InputBox, cb_colorF, vector, '1 1 1')
 +      ATTRIB(InputBox, cb_colorC, vector, '1 1 1')
 +ENDCLASS(InputBox)
- void InputBox_Clear_Click(entity btn, entity me)
- {
-       me.setText(me, "");
- }
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void InputBox_configureInputBox(entity me, string theText, float theCursorPos, float theFontSize, string gfx)
 +{
 +      SUPER(InputBox).configureLabel(me, theText, theFontSize, 0.0);
 +      me.src = gfx;
 +      me.cursorPos = theCursorPos;
 +}
 +void InputBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      SUPER(InputBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +      if (me.enableClearButton)
 +      {
 +              me.cb_width = absSize.y / absSize.x;
 +              me.cb_offset = bound(-1, me.cb_offset, 0) * me.cb_width; // bound to range -1, 0
 +              me.keepspaceRight = me.keepspaceRight - me.cb_offset + me.cb_width;
 +      }
 +}
 +
 +void InputBox_setText(entity me, string txt)
 +{
 +      if(me.text)
 +              strunzone(me.text);
 +      SUPER(InputBox).setText(me, strzone(txt));
 +}
 +
-               InputBox_Clear_Click(world, me);
 +float over_ClearButton(entity me, vector pos)
 +{
 +      if (pos.x >= 1 + me.cb_offset - me.cb_width)
 +      if (pos.x < 1 + me.cb_offset)
 +      if (pos.y >= 0)
 +      if (pos.y < 1)
 +              return 1;
 +      return 0;
 +}
 +
 +float InputBox_mouseMove(entity me, vector pos)
 +{
 +      if (me.enableClearButton)
 +      {
 +              if (over_ClearButton(me, pos))
 +              {
 +                      me.cb_focused = 1;
 +                      return 1;
 +              }
 +              me.cb_focused = 0;
 +      }
 +      return 1;
 +}
 +
 +float InputBox_mouseDrag(entity me, vector pos)
 +{
 +      float p;
 +      if(me.pressed)
 +      {
 +              me.dragScrollPos = pos;
 +              p = me.scrollPos + pos.x - me.keepspaceLeft;
 +              me.cursorPos = draw_TextLengthUpToWidth(me.text, p, 0, me.realFontSize);
 +              me.lastChangeTime = time;
 +      }
 +      else if (me.enableClearButton)
 +      {
 +              if (over_ClearButton(me, pos))
 +              {
 +                      me.cb_pressed = 1;
 +                      return 1;
 +              }
 +      }
 +      me.cb_pressed = 0;
 +      return 1;
 +}
 +
 +float InputBox_mousePress(entity me, vector pos)
 +{
 +      if (me.enableClearButton)
 +      if (over_ClearButton(me, pos))
 +      {
 +              me.cb_pressed = 1;
 +              return 1;
 +      }
 +      me.dragScrollTimer = time;
 +      me.pressed = 1;
 +      return InputBox_mouseDrag(me, pos);
 +}
 +
 +float InputBox_mouseRelease(entity me, vector pos)
 +{
 +      if(me.cb_pressed)
 +      if (over_ClearButton(me, pos))
 +      {
++              m_play_click_sound(MENU_SOUND_CLEAR);
++              me.setText(me, "");
 +              me.cb_pressed = 0;
 +              return 1;
 +      }
 +      float r = InputBox_mouseDrag(me, pos);
 +      //reset cb_pressed after mouseDrag, mouseDrag could set cb_pressed in this case:
 +      //mouse press out of the clear button, drag and then mouse release over the clear button
 +      me.cb_pressed = 0;
 +      me.pressed = 0;
 +      return r;
 +}
 +
 +void InputBox_enterText(entity me, string ch)
 +{
 +      float i;
 +      for(i = 0; i < strlen(ch); ++i)
 +              if(strstrofs(me.forbiddenCharacters, substring(ch, i, 1), 0) > -1)
 +                      return;
 +      if(me.maxLength > 0)
 +      {
 +              if(strlen(ch) + strlen(me.text) > me.maxLength)
 +                      return;
 +      }
 +      else if(me.maxLength < 0)
 +      {
 +              if(u8_strsize(ch) + u8_strsize(me.text) > -me.maxLength)
 +                      return;
 +      }
 +      me.setText(me, strcat(substring(me.text, 0, me.cursorPos), ch, substring(me.text, me.cursorPos, strlen(me.text) - me.cursorPos)));
 +      me.cursorPos += strlen(ch);
 +}
 +
 +float InputBox_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      me.lastChangeTime = time;
 +      me.dragScrollTimer = time;
 +      if(ascii >= 32 && ascii != 127)
 +      {
 +              me.enterText(me, chr(ascii));
 +              return 1;
 +      }
 +      switch(key)
 +      {
 +              case K_KP_LEFTARROW:
 +              case K_LEFTARROW:
 +                      me.cursorPos -= 1;
 +                      return 1;
 +              case K_KP_RIGHTARROW:
 +              case K_RIGHTARROW:
 +                      me.cursorPos += 1;
 +                      return 1;
 +              case K_KP_HOME:
 +              case K_HOME:
 +                      me.cursorPos = 0;
 +                      return 1;
 +              case K_KP_END:
 +              case K_END:
 +                      me.cursorPos = strlen(me.text);
 +                      return 1;
 +              case K_BACKSPACE:
 +                      if(me.cursorPos > 0)
 +                      {
 +                              me.cursorPos -= 1;
 +                              me.setText(me, strcat(substring(me.text, 0, me.cursorPos), substring(me.text, me.cursorPos + 1, strlen(me.text) - me.cursorPos - 1)));
 +                      }
 +                      return 1;
 +              case K_KP_DEL:
 +              case K_DEL:
 +                      if(shift & S_CTRL)
++                      {
++                              m_play_click_sound(MENU_SOUND_CLEAR);
 +                              me.setText(me, "");
++                      }
 +                      else
 +                              me.setText(me, strcat(substring(me.text, 0, me.cursorPos), substring(me.text, me.cursorPos + 1, strlen(me.text) - me.cursorPos - 1)));
 +                      return 1;
 +      }
 +      return 0;
 +}
 +
 +void InputBox_draw(entity me)
 +{
 +      string CURSOR = "_";
 +      float cursorPosInWidths, totalSizeInWidths;
 +
 +      if(me.pressed)
 +              me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
 +
 +      if(me.recalcPos)
 +              me.recalcPositionWithText(me, me.text);
 +
 +      me.focusable = !me.disabled;
 +      if(me.disabled)
 +              draw_alpha *= me.disabledAlpha;
 +
 +      if(me.src)
 +      {
 +              if(me.focused && !me.disabled)
 +                      draw_ButtonPicture('0 0 0', strcat(me.src, "_f"), '1 1 0', me.colorF, 1);
 +              else
 +                      draw_ButtonPicture('0 0 0', strcat(me.src, "_n"), '1 1 0', me.color, 1);
 +      }
 +
 +      me.cursorPos = bound(0, me.cursorPos, strlen(me.text));
 +      cursorPosInWidths = draw_TextWidth(substring(me.text, 0, me.cursorPos), 0, me.realFontSize);
 +      totalSizeInWidths = draw_TextWidth(strcat(me.text, CURSOR), 0, me.realFontSize);
 +
 +      if(me.dragScrollTimer < time)
 +      {
 +              float save;
 +              save = me.scrollPos;
 +              me.scrollPos = bound(cursorPosInWidths - (0.875 - me.keepspaceLeft - me.keepspaceRight), me.scrollPos, cursorPosInWidths - 0.125);
 +              if(me.scrollPos != save)
 +                      me.dragScrollTimer = time + 0.2;
 +      }
 +      me.scrollPos = min(me.scrollPos, totalSizeInWidths - (1 - me.keepspaceRight - me.keepspaceLeft));
 +      me.scrollPos = max(0, me.scrollPos);
 +
 +      draw_SetClipRect(eX * me.keepspaceLeft, eX * (1 - me.keepspaceLeft - me.keepspaceRight) + eY);
 +      if(me.editColorCodes)
 +      {
 +              string ch, ch2;
 +              float i, n;
 +              vector theColor;
 +              float theAlpha;    //float theVariableAlpha;
 +              vector p;
 +              vector theTempColor;
 +              float component;
 +
 +              p = me.realOrigin - eX * me.scrollPos;
 +              theColor = '1 1 1';
 +              theAlpha = 1;    //theVariableAlpha = 1; // changes when ^ax found
 +
 +              n = strlen(me.text);
 +              for(i = 0; i < n; ++i)
 +              {
 +                      ch = substring(me.text, i, 1);
 +                      if(ch == "^")
 +                      {
 +                              float w;
 +                              ch2 = substring(me.text, i+1, 1);
 +                              w = draw_TextWidth(strcat(ch, ch2), 0, me.realFontSize);
 +                              if(ch2 == "^")
 +                              {
 +                                      draw_Fill(p, eX * w + eY * me.realFontSize.y, '1 1 1', 0.5);
 +                                      draw_Text(p + eX * 0.25 * w, "^", me.realFontSize, theColor, theAlpha, 0);
 +                              }
 +                              else if(ch2 == "0" || stof(ch2)) // digit?
 +                              {
 +                                      switch(stof(ch2))
 +                                      {
 +                                              case 0: theColor = '0 0 0'; theAlpha = 1; break;
 +                                              case 1: theColor = '1 0 0'; theAlpha = 1; break;
 +                                              case 2: theColor = '0 1 0'; theAlpha = 1; break;
 +                                              case 3: theColor = '1 1 0'; theAlpha = 1; break;
 +                                              case 4: theColor = '0 0 1'; theAlpha = 1; break;
 +                                              case 5: theColor = '0 1 1'; theAlpha = 1; break;
 +                                              case 6: theColor = '1 0 1'; theAlpha = 1; break;
 +                                              case 7: theColor = '1 1 1'; theAlpha = 1; break;
 +                                              case 8: theColor = '1 1 1'; theAlpha = 0.5; break;
 +                                              case 9: theColor = '0.5 0.5 0.5'; theAlpha = 1; break;
 +                                      }
 +                                      draw_Fill(p, eX * w + eY * me.realFontSize.y, '1 1 1', 0.5);
 +                                      draw_Text(p, strcat(ch, ch2), me.realFontSize, theColor, theAlpha, 0);
 +                              }
 +                              else if(ch2 == "x") // ^x found
 +                              {
 +                                      theColor = '1 1 1';
 +
 +                                      component = HEXDIGIT_TO_DEC(substring(me.text, i+2, 1));
 +                                      if (component >= 0) // ^xr found
 +                                      {
 +                                              theTempColor.x = component/15;
 +
 +                                              component = HEXDIGIT_TO_DEC(substring(me.text, i+3, 1));
 +                                              if (component >= 0) // ^xrg found
 +                                              {
 +                                                      theTempColor.y = component/15;
 +
 +                                                      component = HEXDIGIT_TO_DEC(substring(me.text, i+4, 1));
 +                                                      if (component >= 0) // ^xrgb found
 +                                                      {
 +                                                              theTempColor.z = component/15;
 +                                                              theColor = theTempColor;
 +                                                              w = draw_TextWidth(substring(me.text, i, 5), 0, me.realFontSize);
 +
 +                                                              draw_Fill(p, eX * w + eY * me.realFontSize.y, '1 1 1', 0.5);
 +                                                              draw_Text(p, substring(me.text, i, 5), me.realFontSize, theColor, 1, 0);    // theVariableAlpha instead of 1 using alpha tags ^ax
 +                                                              i += 3;
 +                                                      }
 +                                                      else
 +                                                      {
 +                                                              // blue missing
 +                                                              w = draw_TextWidth(substring(me.text, i, 4), 0, me.realFontSize);
 +                                                              draw_Fill(p, eX * w + eY * me.realFontSize.y, eZ, 0.5);
 +                                                              draw_Text(p, substring(me.text, i, 4), me.realFontSize, '1 1 1', theAlpha, 0);
 +                                                              i += 2;
 +                                                      }
 +                                              }
 +                                              else
 +                                              {
 +                                                      // green missing
 +                                                      w = draw_TextWidth(substring(me.text, i, 3), 0, me.realFontSize);
 +                                                      draw_Fill(p, eX * w + eY * me.realFontSize.y, eY, 0.5);
 +                                                      draw_Text(p, substring(me.text, i, 3), me.realFontSize, '1 1 1', theAlpha, 0);
 +                                                      i += 1;
 +                                              }
 +                                      }
 +                                      else
 +                                      {
 +                                              // red missing
 +                                              //w = draw_TextWidth(substring(me.text, i, 2), 0) * me.realFontSize_x;
 +                                              draw_Fill(p, eX * w + eY * me.realFontSize.y, eX, 0.5);
 +                                              draw_Text(p, substring(me.text, i, 2), me.realFontSize, '1 1 1', theAlpha, 0);
 +                                      }
 +                              }
 +                              else
 +                              {
 +                                      draw_Fill(p, eX * w + eY * me.realFontSize.y, '1 1 1', 0.5);
 +                                      draw_Text(p, strcat(ch, ch2), me.realFontSize, theColor, theAlpha, 0);
 +                              }
 +                              p += w * eX;
 +                              ++i;
 +                              continue;
 +                      }
 +                      draw_Text(p, ch, me.realFontSize, theColor, theAlpha, 0); // TODO theVariableAlpha
 +                      p += eX * draw_TextWidth(ch, 0, me.realFontSize);
 +              }
 +      }
 +      else
 +              draw_Text(me.realOrigin - eX * me.scrollPos, me.text, me.realFontSize, '1 1 1', 1, 0);
 +
 +      if(!me.focused || (time - me.lastChangeTime) < floor(time - me.lastChangeTime) + 0.5)
 +              draw_Text(me.realOrigin + eX * (cursorPosInWidths - me.scrollPos), CURSOR, me.realFontSize, '1 1 1', 1, 0);
 +
 +      draw_ClearClip();
 +
 +      if (me.enableClearButton)
 +      if (me.text != "")
 +      {
 +              if(me.focused && me.cb_pressed)
 +                      draw_Picture(eX * (1 + me.cb_offset - me.cb_width), strcat(me.cb_src, "_c"), eX * me.cb_width + eY, me.cb_colorC, 1);
 +              else if(me.focused && me.cb_focused)
 +                      draw_Picture(eX * (1 + me.cb_offset - me.cb_width), strcat(me.cb_src, "_f"), eX * me.cb_width + eY, me.cb_colorF, 1);
 +              else
 +                      draw_Picture(eX * (1 + me.cb_offset - me.cb_width), strcat(me.cb_src, "_n"), eX * me.cb_width + eY, me.cb_color, 1);
 +      }
 +
 +      // skipping SUPER(InputBox).draw(me);
 +      Item_draw(me);
 +}
 +
 +void InputBox_showNotify(entity me)
 +{
 +      me.focusable = !me.disabled;
 +}
 +#endif
index 2f29795,0000000..178b12b
mode 100644,000000..100644
--- /dev/null
@@@ -1,402 -1,0 +1,403 @@@
 +#ifdef INTERFACE
 +CLASS(ListBox) EXTENDS(Item)
 +      METHOD(ListBox, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(ListBox, configureListBox, void(entity, float, float))
 +      METHOD(ListBox, draw, void(entity))
 +      METHOD(ListBox, keyDown, float(entity, float, float, float))
 +      METHOD(ListBox, mousePress, float(entity, vector))
 +      METHOD(ListBox, mouseDrag, float(entity, vector))
 +      METHOD(ListBox, mouseRelease, float(entity, vector))
 +      METHOD(ListBox, focusLeave, void(entity))
 +      ATTRIB(ListBox, focusable, float, 1)
++      ATTRIB(ListBox, allowFocusSound, float, 1)
 +      ATTRIB(ListBox, selectedItem, float, 0)
 +      ATTRIB(ListBox, size, vector, '0 0 0')
 +      ATTRIB(ListBox, origin, vector, '0 0 0')
 +      ATTRIB(ListBox, scrollPos, float, 0) // measured in window heights, fixed when needed
 +      ATTRIB(ListBox, previousValue, float, 0)
 +      ATTRIB(ListBox, pressed, float, 0) // 0 = normal, 1 = scrollbar dragging, 2 = item dragging, 3 = released
 +      ATTRIB(ListBox, pressOffset, float, 0)
 +
 +      METHOD(ListBox, updateControlTopBottom, void(entity))
 +      ATTRIB(ListBox, controlTop, float, 0)
 +      ATTRIB(ListBox, controlBottom, float, 0)
 +      ATTRIB(ListBox, controlWidth, float, 0)
 +      ATTRIB(ListBox, dragScrollTimer, float, 0)
 +      ATTRIB(ListBox, dragScrollPos, vector, '0 0 0')
 +
 +      ATTRIB(ListBox, src, string, string_null) // scrollbar
 +      ATTRIB(ListBox, color, vector, '1 1 1')
 +      ATTRIB(ListBox, color2, vector, '1 1 1')
 +      ATTRIB(ListBox, colorC, vector, '1 1 1')
 +      ATTRIB(ListBox, colorF, vector, '1 1 1')
 +      ATTRIB(ListBox, tolerance, vector, '0 0 0') // drag tolerance
 +      ATTRIB(ListBox, scrollbarWidth, float, 0) // pixels
 +      ATTRIB(ListBox, nItems, float, 42)
 +      ATTRIB(ListBox, itemHeight, float, 0)
 +      ATTRIB(ListBox, colorBG, vector, '0 0 0')
 +      ATTRIB(ListBox, alphaBG, float, 0)
 +
 +      ATTRIB(ListBox, lastClickedItem, float, -1)
 +      ATTRIB(ListBox, lastClickedTime, float, 0)
 +
 +      METHOD(ListBox, drawListBoxItem, void(entity, float, vector, float)) // item number, width/height, selected
 +      METHOD(ListBox, clickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
 +      METHOD(ListBox, doubleClickListBoxItem, void(entity, float, vector)) // item number, relative clickpos
 +      METHOD(ListBox, setSelected, void(entity, float))
 +
 +      METHOD(ListBox, getLastFullyVisibleItemAtScrollPos, float(entity, float))
 +      METHOD(ListBox, getFirstFullyVisibleItemAtScrollPos, float(entity, float))
 +
 +      // NOTE: override these four methods if you want variable sized list items
 +      METHOD(ListBox, getTotalHeight, float(entity))
 +      METHOD(ListBox, getItemAtPos, float(entity, float))
 +      METHOD(ListBox, getItemStart, float(entity, float))
 +      METHOD(ListBox, getItemHeight, float(entity, float))
 +      // NOTE: if getItemAt* are overridden, it may make sense to cache the
 +      // start and height of the last item returned by getItemAtPos and fast
 +      // track returning their properties for getItemStart and getItemHeight.
 +      // The "hot" code path calls getItemAtPos first, then will query
 +      // getItemStart and getItemHeight on it soon.
 +      // When overriding, the following consistency rules must hold:
 +      // getTotalHeight() == SUM(getItemHeight(i), i, 0, me.nItems-1)
 +      // getItemStart(i+1) == getItemStart(i) + getItemHeight(i)
 +      //   for 0 <= i < me.nItems-1
 +      // getItemStart(0) == 0
 +      // getItemStart(getItemAtPos(p)) <= p
 +      //   if p >= 0
 +      // getItemAtPos(p) == 0
 +      //   if p < 0
 +      // getItemStart(getItemAtPos(p)) + getItemHeight(getItemAtPos(p)) > p
 +      //   if p < getTotalHeigt()
 +      // getItemAtPos(p) == me.nItems - 1
 +      //   if p >= getTotalHeight()
 +ENDCLASS(ListBox)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void ListBox_setSelected(entity me, float i)
 +{
 +      me.selectedItem = bound(0, i, me.nItems - 1);
 +}
 +void ListBox_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      SUPER(ListBox).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +      me.controlWidth = me.scrollbarWidth / absSize.x;
 +}
 +void ListBox_configureListBox(entity me, float theScrollbarWidth, float theItemHeight)
 +{
 +      me.scrollbarWidth = theScrollbarWidth;
 +      me.itemHeight = theItemHeight;
 +}
 +
 +float ListBox_getTotalHeight(entity me)
 +{
 +      return me.nItems * me.itemHeight;
 +}
 +float ListBox_getItemAtPos(entity me, float pos)
 +{
 +      return floor(pos / me.itemHeight);
 +}
 +float ListBox_getItemStart(entity me, float i)
 +{
 +      return me.itemHeight * i;
 +}
 +float ListBox_getItemHeight(entity me, float i)
 +{
 +      return me.itemHeight;
 +}
 +
 +float ListBox_getLastFullyVisibleItemAtScrollPos(entity me, float pos)
 +{
 +      return me.getItemAtPos(me, pos + 1.001) - 1;
 +}
 +float ListBox_getFirstFullyVisibleItemAtScrollPos(entity me, float pos)
 +{
 +      return me.getItemAtPos(me, pos - 0.001) + 1;
 +}
 +float ListBox_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      me.dragScrollTimer = time;
 +      if(key == K_MWHEELUP)
 +      {
 +              me.scrollPos = max(me.scrollPos - 0.5, 0);
 +              me.setSelected(me, min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
 +      }
 +      else if(key == K_MWHEELDOWN)
 +      {
 +              me.scrollPos = min(me.scrollPos + 0.5, me.getTotalHeight(me) - 1);
 +              me.setSelected(me, max(me.selectedItem, me.getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
 +      }
 +      else if(key == K_PGUP || key == K_KP_PGUP)
 +      {
 +              float i = me.selectedItem;
 +              float a = me.getItemHeight(me, i);
 +              for (;;)
 +              {
 +                      --i;
 +                      if (i < 0)
 +                              break;
 +                      a += me.getItemHeight(me, i);
 +                      if (a >= 1)
 +                              break;
 +              }
 +              me.setSelected(me, i + 1);
 +      }
 +      else if(key == K_PGDN || key == K_KP_PGDN)
 +      {
 +              float i = me.selectedItem;
 +              float a = me.getItemHeight(me, i);
 +              for (;;)
 +              {
 +                      ++i;
 +                      if (i >= me.nItems)
 +                              break;
 +                      a += me.getItemHeight(me, i);
 +                      if (a >= 1)
 +                              break;
 +              }
 +              me.setSelected(me, i - 1);
 +      }
 +      else if(key == K_UPARROW || key == K_KP_UPARROW)
 +              me.setSelected(me, me.selectedItem - 1);
 +      else if(key == K_DOWNARROW || key == K_KP_DOWNARROW)
 +              me.setSelected(me, me.selectedItem + 1);
 +      else if(key == K_HOME || key == K_KP_HOME)
 +      {
 +              me.scrollPos = 0;
 +              me.setSelected(me, 0);
 +      }
 +      else if(key == K_END || key == K_KP_END)
 +      {
 +              me.scrollPos = max(0, me.getTotalHeight(me) - 1);
 +              me.setSelected(me, me.nItems - 1);
 +      }
 +      else
 +              return 0;
 +      return 1;
 +}
 +float ListBox_mouseDrag(entity me, vector pos)
 +{
 +      float hit;
 +      float i;
 +      me.updateControlTopBottom(me);
 +      me.dragScrollPos = pos;
 +      if(me.pressed == 1)
 +      {
 +              hit = 1;
 +              if(pos.x < 1 - me.controlWidth - me.tolerance.y * me.controlWidth) hit = 0;
 +              if(pos.y < 0 - me.tolerance.x) hit = 0;
 +              if(pos.x >= 1 + me.tolerance.y * me.controlWidth) hit = 0;
 +              if(pos.y >= 1 + me.tolerance.x) hit = 0;
 +              if(hit)
 +              {
 +                      // calculate new pos to v
 +                      float d;
 +                      d = (pos.y - me.pressOffset) / (1 - (me.controlBottom - me.controlTop)) * (me.getTotalHeight(me) - 1);
 +                      me.scrollPos = me.previousValue + d;
 +              }
 +              else
 +                      me.scrollPos = me.previousValue;
 +              me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
 +              me.scrollPos = max(me.scrollPos, 0);
 +              i = min(me.selectedItem, me.getLastFullyVisibleItemAtScrollPos(me, me.scrollPos));
 +              i = max(i, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos));
 +              me.setSelected(me, i);
 +      }
 +      else if(me.pressed == 2)
 +      {
 +              me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
 +      }
 +      return 1;
 +}
 +float ListBox_mousePress(entity me, vector pos)
 +{
 +      if(pos.x < 0) return 0;
 +      if(pos.y < 0) return 0;
 +      if(pos.x >= 1) return 0;
 +      if(pos.y >= 1) return 0;
 +      me.dragScrollPos = pos;
 +      me.updateControlTopBottom(me);
 +      me.dragScrollTimer = time;
 +      if(pos.x >= 1 - me.controlWidth)
 +      {
 +              // if hit, set me.pressed, otherwise scroll by one page
 +              if(pos.y < me.controlTop)
 +              {
 +                      // page up
 +                      me.scrollPos = max(me.scrollPos - 1, 0);
 +                      me.setSelected(me, min(me.selectedItem, ListBox_getLastFullyVisibleItemAtScrollPos(me, me.scrollPos)));
 +              }
 +              else if(pos.y > me.controlBottom)
 +              {
 +                      // page down
 +                      me.scrollPos = min(me.scrollPos + 1, me.getTotalHeight(me) - 1);
 +                      me.setSelected(me, max(me.selectedItem, ListBox_getFirstFullyVisibleItemAtScrollPos(me, me.scrollPos)));
 +              }
 +              else
 +              {
 +                      me.pressed = 1;
 +                      me.pressOffset = pos.y;
 +                      me.previousValue = me.scrollPos;
 +              }
 +      }
 +      else
 +      {
 +              // continue doing that while dragging (even when dragging outside). When releasing, forward the click to the then selected item.
 +              me.pressed = 2;
 +              // an item has been clicked. Select it, ...
 +              me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
 +      }
 +      return 1;
 +}
 +float ListBox_mouseRelease(entity me, vector pos)
 +{
 +      if(me.pressed == 1)
 +      {
 +              // slider dragging mode
 +              // in that case, nothing happens on releasing
 +      }
 +      else if(me.pressed == 2)
 +      {
 +              me.pressed = 3; // do that here, so setSelected can know the mouse has been released
 +              // item dragging mode
 +              // select current one one last time...
 +              me.setSelected(me, me.getItemAtPos(me, me.scrollPos + pos.y));
 +              // and give it a nice click event
 +              if(me.nItems > 0)
 +              {
 +                      vector where = globalToBox(pos, eY * (me.getItemStart(me, me.selectedItem) - me.scrollPos), eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, me.selectedItem));
 +
 +                      if((me.selectedItem == me.lastClickedItem) && (time < me.lastClickedTime + 0.3))
 +                              me.doubleClickListBoxItem(me, me.selectedItem, where);
 +                      else
 +                              me.clickListBoxItem(me, me.selectedItem, where);
 +
 +                      me.lastClickedItem = me.selectedItem;
 +                      me.lastClickedTime = time;
 +              }
 +      }
 +      me.pressed = 0;
 +      return 1;
 +}
 +void ListBox_focusLeave(entity me)
 +{
 +      // Reset the var pressed in case listbox loses focus
 +      // by a mouse click on an item of the list
 +      // for example showing a dialog on right click
 +      me.pressed = 0;
 +}
 +void ListBox_updateControlTopBottom(entity me)
 +{
 +      float f;
 +      // scrollPos is in 0..1 and indicates where the "page" currently shown starts.
 +      if(me.getTotalHeight(me) <= 1)
 +      {
 +              // we don't need no stinkin' scrollbar, we don't need no view control...
 +              me.controlTop = 0;
 +              me.controlBottom = 1;
 +              me.scrollPos = 0;
 +      }
 +      else
 +      {
 +              if(frametime) // only do this in draw frames
 +              {
 +                      if(me.dragScrollTimer < time)
 +                      {
 +                              float save;
 +                              save = me.scrollPos;
 +                              // if selected item is below listbox, increase scrollpos so it is in
 +                              me.scrollPos = max(me.scrollPos, me.getItemStart(me, me.selectedItem) + me.getItemHeight(me, me.selectedItem) - 1);
 +                              // if selected item is above listbox, decrease scrollpos so it is in
 +                              me.scrollPos = min(me.scrollPos, me.getItemStart(me, me.selectedItem));
 +                              if(me.scrollPos != save)
 +                                      me.dragScrollTimer = time + 0.2;
 +                      }
 +              }
 +              // if scroll pos is below end of list, fix it
 +              me.scrollPos = min(me.scrollPos, me.getTotalHeight(me) - 1);
 +              // if scroll pos is above beginning of list, fix it
 +              me.scrollPos = max(me.scrollPos, 0);
 +              // now that we know where the list is scrolled to, find out where to draw the control
 +              me.controlTop = max(0, me.scrollPos / me.getTotalHeight(me));
 +              me.controlBottom = min((me.scrollPos + 1) / me.getTotalHeight(me), 1);
 +
 +              float minfactor;
 +              minfactor = 2 * me.controlWidth / me.size.y * me.size.x;
 +              f = me.controlBottom - me.controlTop;
 +              if(f < minfactor) // FIXME good default?
 +              {
 +                      // f * X + 1 * (1-X) = minfactor
 +                      // (f - 1) * X + 1 = minfactor
 +                      // (f - 1) * X = minfactor - 1
 +                      // X = (minfactor - 1) / (f - 1)
 +                      f = (minfactor - 1) / (f - 1);
 +                      me.controlTop = me.controlTop * f + 0 * (1 - f);
 +                      me.controlBottom = me.controlBottom * f + 1 * (1 - f);
 +              }
 +      }
 +}
 +void ListBox_draw(entity me)
 +{
 +      float i;
 +      vector absSize, fillSize = '0 0 0';
 +      vector oldshift, oldscale;
 +      if(me.pressed == 2)
 +              me.mouseDrag(me, me.dragScrollPos); // simulate mouseDrag event
 +      me.updateControlTopBottom(me);
 +      fillSize.x = (1 - me.controlWidth);
 +      if(me.alphaBG)
 +              draw_Fill('0 0 0', '0 1 0' + fillSize, me.colorBG, me.alphaBG);
 +      if(me.controlWidth)
 +      {
 +              draw_VertButtonPicture(eX * (1 - me.controlWidth), strcat(me.src, "_s"), eX * me.controlWidth + eY, me.color2, 1);
 +              if(me.getTotalHeight(me) > 1)
 +              {
 +                      vector o, s;
 +                      o = eX * (1 - me.controlWidth) + eY * me.controlTop;
 +                      s = eX * me.controlWidth + eY * (me.controlBottom - me.controlTop);
 +                      if(me.pressed == 1)
 +                              draw_VertButtonPicture(o, strcat(me.src, "_c"), s, me.colorC, 1);
 +                      else if(me.focused)
 +                              draw_VertButtonPicture(o, strcat(me.src, "_f"), s, me.colorF, 1);
 +                      else
 +                              draw_VertButtonPicture(o, strcat(me.src, "_n"), s, me.color, 1);
 +              }
 +      }
 +      draw_SetClip();
 +      oldshift = draw_shift;
 +      oldscale = draw_scale;
 +      float y;
 +      i = me.getItemAtPos(me, me.scrollPos);
 +      y = me.getItemStart(me, i) - me.scrollPos;
 +      for (; i < me.nItems && y < 1; ++i)
 +      {
 +              draw_shift = boxToGlobal(eY * y, oldshift, oldscale);
 +              vector relSize = eX * (1 - me.controlWidth) + eY * me.getItemHeight(me, i);
 +              absSize = boxToGlobalSize(relSize, me.size);
 +              draw_scale = boxToGlobalSize(relSize, oldscale);
 +              me.drawListBoxItem(me, i, absSize, (me.selectedItem == i));
 +              y += relSize.y;
 +      }
 +      draw_ClearClip();
 +
 +      draw_shift = oldshift;
 +      draw_scale = oldscale;
 +      SUPER(ListBox).draw(me);
 +}
 +
 +void ListBox_clickListBoxItem(entity me, float i, vector where)
 +{
 +      // template method
 +}
 +
 +void ListBox_doubleClickListBoxItem(entity me, float i, vector where)
 +{
 +      // template method
 +}
 +
 +void ListBox_drawListBoxItem(entity me, float i, vector absSize, float selected)
 +{
 +      draw_Text('0 0 0', sprintf(_("Item %d"), i), eX * (8 / absSize.x) + eY * (8 / absSize.y), (selected ? '0 1 0' : '1 1 1'), 1, 0);
 +}
 +#endif
index 38332fd,0000000..bff2170
mode 100644,000000..100644
--- /dev/null
@@@ -1,293 -1,0 +1,305 @@@
-               f = (e.ModalController_factor = min(1, e.ModalController_factor + df));
-               if(e.ModalController_state)
-                       if(f < 1)
-                               animating = 1;
-               if(f < 1)
-               {
-                       prevFactor   = (1 - f) / (1 - f + df);
-                       targetFactor =     df  / (1 - f + df);
-               }
-               else
-               {
-                       prevFactor = 0;
-                       targetFactor = 1;
-               }
 +#ifdef INTERFACE
 +CLASS(ModalController) EXTENDS(Container)
 +      METHOD(ModalController, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(ModalController, draw, void(entity))
 +      METHOD(ModalController, showChild, void(entity, entity, vector, vector, float))
 +      METHOD(ModalController, hideChild, void(entity, entity, float))
 +      METHOD(ModalController, hideAll, void(entity, float))
 +      METHOD(ModalController, addItem, void(entity, entity, vector, vector, float))
 +      METHOD(ModalController, addTab, void(entity, entity, entity))
 +
 +      METHOD(ModalController, initializeDialog, void(entity, entity))
 +
 +      METHOD(ModalController, switchState, void(entity, entity, float, float))
 +      ATTRIB(ModalController, origin, vector, '0 0 0')
 +      ATTRIB(ModalController, size, vector, '0 0 0')
 +      ATTRIB(ModalController, previousButton, entity, NULL)
 +      ATTRIB(ModalController, fadedAlpha, float, 0.3)
 +ENDCLASS(ModalController)
 +
 +.entity tabSelectingButton;
 +.vector origin;
 +.vector size;
 +void TabButton_Click(entity button, entity tab); // assumes a button has set the above fields to its own absolute origin, its size, and the tab to activate
 +void DialogOpenButton_Click(entity button, entity tab); // assumes a button has set the above fields to its own absolute origin, its size, and the tab to activate
 +void DialogOpenButton_Click_withCoords(entity button, entity tab, vector theOrigin, vector theSize);
 +void DialogCloseButton_Click(entity button, entity tab); // assumes a button has set the above fields to the tab to close
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +
 +// modal dialog controller
 +// handles a stack of dialog elements
 +// each element can have one of the following states:
 +//   0: hidden (fading out)
 +//   1: visible (zooming in)
 +//   2: greyed out (inactive)
 +// While an animation is running, no item has focus. When an animation is done,
 +// the topmost item gets focus.
 +// The items are assumed to be added in overlapping order, that is, the lowest
 +// window must get added first.
 +//
 +// Possible uses:
 +// - to control a modal dialog:
 +//   - show modal dialog: me.showChild(me, childItem, buttonAbsOrigin, buttonAbsSize, 0) // childItem also gets focus
 +//   - dismiss modal dialog: me.hideChild(me, childItem, 0) // childItem fades out and relinquishes focus
 +//   - show first screen in m_show: me.hideAll(me, 1); me.showChild(me, me.firstChild, '0 0 0', '0 0 0', 1);
 +// - to show a temporary dialog instead of the menu (teamselect): me.hideAll(me, 1); me.showChild(me, teamSelectDialog, '0 0 0', '0 0 0', 1);
 +// - as a tabbed dialog control:
 +//   - to initialize: me.hideAll(me, 1); me.showChild(me, me.firstChild, '0 0 0', '0 0 0', 1);
 +//   - to show a tab: me.hideChild(me, currentTab, 0); me.showChild(me, newTab, buttonAbsOrigin, buttonAbsSize, 0);
 +
 +.vector ModalController_initialSize;
 +.vector ModalController_initialOrigin;
 +.vector ModalController_initialFontScale;
 +.float ModalController_initialAlpha;
 +.vector ModalController_buttonSize;
 +.vector ModalController_buttonOrigin;
 +.float ModalController_state;
 +.float ModalController_factor;
 +.entity ModalController_controllingButton;
 +
 +void ModalController_initializeDialog(entity me, entity root)
 +{
 +      me.hideAll(me, 1);
 +      me.showChild(me, root, '0 0 0', '0 0 0', 1); // someone else animates for us
 +}
 +
 +void TabButton_Click(entity button, entity tab)
 +{
 +      if(tab.ModalController_state == 1)
 +              return;
 +      tab.parent.hideAll(tab.parent, 0);
 +      button.forcePressed = 1;
 +      tab.ModalController_controllingButton = button;
 +      tab.parent.showChild(tab.parent, tab, button.origin, button.size, 0);
 +}
 +
 +void DialogOpenButton_Click(entity button, entity tab)
 +{
 +      DialogOpenButton_Click_withCoords(button, tab, button.origin, button.size);
 +}
 +
 +void DialogOpenButton_Click_withCoords(entity button, entity tab, vector theOrigin, vector theSize)
 +{
 +      if(tab.ModalController_state)
 +              return;
 +      if(button)
 +              button.forcePressed = 1;
 +      if(tab.parent.focusedChild)
 +              tab.parent.focusedChild.saveFocus(tab.parent.focusedChild);
 +      tab.ModalController_controllingButton = button;
 +      tab.parent.showChild(tab.parent, tab, theOrigin, theSize, 0);
 +}
 +
 +void DialogCloseButton_Click(entity button, entity tab)
 +{
 +      tab.parent.hideChild(tab.parent, tab, 0);
 +}
 +
 +void ModalController_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.resizeNotifyLie(me, relOrigin, relSize, absOrigin, absSize, ModalController_initialOrigin, ModalController_initialSize, ModalController_initialFontScale);
 +}
 +
 +void ModalController_switchState(entity me, entity other, float state, float skipAnimation)
 +{
 +      float previousState;
 +      previousState = other.ModalController_state;
 +      if(state == previousState && !skipAnimation)
 +              return;
 +      other.ModalController_state = state;
 +      switch(state)
 +      {
 +              case 0:
 +                      other.ModalController_factor = 1 - other.Container_alpha / other.ModalController_initialAlpha;
 +                      // fading out
 +                      break;
 +              case 1:
 +                      other.ModalController_factor = other.Container_alpha / other.ModalController_initialAlpha;
 +                      if(previousState == 0 && !skipAnimation)
 +                      {
 +                              other.Container_origin = other.ModalController_buttonOrigin;
 +                              other.Container_size = other.ModalController_buttonSize;
 +                      }
 +                      // zooming in
 +                      break;
 +              case 2:
 +                      other.ModalController_factor = bound(0, (1 - other.Container_alpha / other.ModalController_initialAlpha) / me.fadedAlpha, 1);
 +                      // fading out halfway
 +                      break;
 +      }
 +      if(skipAnimation)
 +              other.ModalController_factor = 1;
 +}
 +
 +void ModalController_draw(entity me)
 +{
 +      entity e;
 +      entity front;
 +      float animating;
 +      float f; // animation factor
 +      float df; // animation step size
 +      float prevFactor, targetFactor;
 +      vector targetOrigin, targetSize; float targetAlpha;
 +      vector fs;
 +      animating = 0;
 +
 +      front = world;
 +      for(e = me.firstChild; e; e = e.nextSibling)
 +              if(e.ModalController_state)
 +              {
 +                      if(front)
++                      {
 +                              me.switchState(me, front, 2, 0);
++                              if(front.ModalController_factor < 1)
++                                      animating = 1;
++                      }
 +                      front = e;
 +              }
 +      if(front)
++      {
 +              me.switchState(me, front, 1, 0);
++              if(front.ModalController_factor < 1)
++                      animating = 1;
++      }
++
++      if(front && front.Container_alpha == front.ModalController_initialAlpha)
++              goto update_done; // update isn't needed, everything stay as is
 +
 +      df = frametime * 3; // animation speed
 +
 +      for(e = me.firstChild; e; e = e.nextSibling)
 +      {
-                       if(f < 1)
-                               animating = 1;
 +              if(e.ModalController_state == 2)
 +              {
 +                      // fading out partially
 +                      targetOrigin = e.Container_origin; // stay as is
 +                      targetSize = e.Container_size; // stay as is
 +                      targetAlpha = me.fadedAlpha * e.ModalController_initialAlpha;
 +              }
 +              else if(e.ModalController_state == 1)
 +              {
 +                      // zooming in
 +                      targetOrigin = e.ModalController_initialOrigin;
 +                      targetSize = e.ModalController_initialSize;
 +                      targetAlpha = e.ModalController_initialAlpha;
 +              }
 +              else
 +              {
 +                      // fading out
-                       e.Container_origin = e.Container_origin * prevFactor + targetOrigin * targetFactor;
-                       e.Container_size   = e.Container_size   * prevFactor + targetSize   * targetFactor;
-                       me.setAlphaOf(me, e, e.Container_alpha  * prevFactor + targetAlpha  * targetFactor);
 +                      targetOrigin = e.Container_origin; // stay as is
 +                      targetSize = e.Container_size; // stay as is
 +                      targetAlpha = 0;
 +              }
 +
++              f = (e.ModalController_factor = min(1, e.ModalController_factor + df));
 +              if(f == 1)
 +              {
++                      prevFactor = 0;
++                      targetFactor = 1;
 +                      e.Container_origin = targetOrigin;
 +                      e.Container_size = targetSize;
 +                      me.setAlphaOf(me, e, targetAlpha);
 +              }
 +              else
 +              {
-               fs = globalToBoxSize(e.Container_size, e.ModalController_initialSize);
-               e.Container_fontscale_x = fs.x * e.ModalController_initialFontScale.x;
-               e.Container_fontscale_y = fs.y * e.ModalController_initialFontScale.y;
++                      prevFactor = (1 - f) / (1 - f + df);
++                      if(!e.ModalController_state) // optimize code and avoid precision errors
++                              me.setAlphaOf(me, e, e.Container_alpha  * prevFactor);
++                      else
++                      {
++                              targetFactor = df / (1 - f + df);
++
++                              if(e.ModalController_state == 1)
++                              {
++                                      e.Container_origin = e.Container_origin * prevFactor + targetOrigin * targetFactor;
++                                      e.Container_size   = e.Container_size   * prevFactor + targetSize   * targetFactor;
++                              }
++                              me.setAlphaOf(me, e, e.Container_alpha  * prevFactor + targetAlpha  * targetFactor);
++                      }
 +              }
 +              // assume: o == to * f_prev + X * (1 - f_prev)
 +              // make:   o' = to * f  + X * (1 - f)
 +              // -->
 +              // X == (o - to * f_prev) / (1 - f_prev)
 +              // o' = to * f + (o - to * f_prev) / (1 - f_prev) * (1 - f)
 +              // --> (maxima)
 +              // o' = (to * (f - f_prev) + o * (1 - f)) / (1 - f_prev)
 +
++              if(e.ModalController_state == 1)
++              {
++                      fs = globalToBoxSize(e.Container_size, e.ModalController_initialSize);
++                      e.Container_fontscale_x = fs.x * e.ModalController_initialFontScale.x;
++                      e.Container_fontscale_y = fs.y * e.ModalController_initialFontScale.y;
++              }
 +      }
++      :update_done
++
 +      if(animating || !me.focused)
 +              me.setFocus(me, NULL);
 +      else
 +              me.setFocus(me, front);
 +      SUPER(ModalController).draw(me);
 +}
 +
 +void ModalController_addTab(entity me, entity other, entity tabButton)
 +{
 +      me.addItem(me, other, '0 0 0', '1 1 1', 1);
 +      tabButton.onClick = TabButton_Click;
 +      tabButton.onClickEntity = other;
 +      other.tabSelectingButton = tabButton;
 +      if(other == me.firstChild)
 +      {
 +              tabButton.forcePressed = 1;
 +              other.ModalController_controllingButton = tabButton;
 +              me.showChild(me, other, '0 0 0', '0 0 0', 1);
 +      }
 +}
 +
 +void ModalController_addItem(entity me, entity other, vector theOrigin, vector theSize, float theAlpha)
 +{
 +      SUPER(ModalController).addItem(me, other, theOrigin, theSize, (other == me.firstChild) ? theAlpha : 0);
 +      other.ModalController_initialFontScale = other.Container_fontscale;
 +      other.ModalController_initialSize = other.Container_size;
 +      other.ModalController_initialOrigin = other.Container_origin;
 +      other.ModalController_initialAlpha = theAlpha; // hope Container never modifies this
 +      if(other.ModalController_initialFontScale == '0 0 0')
 +              other.ModalController_initialFontScale = '1 1 0';
 +}
 +
 +void ModalController_showChild(entity me, entity other, vector theOrigin, vector theSize, float skipAnimation)
 +{
 +      if(other.ModalController_state == 0 || skipAnimation)
 +      {
 +              me.setFocus(me, NULL);
 +              if(!skipAnimation)
 +              {
 +                      other.ModalController_buttonOrigin = globalToBox(theOrigin, me.origin, me.size);
 +                      other.ModalController_buttonSize = globalToBoxSize(theSize, me.size);
 +              }
 +              me.switchState(me, other, 1, skipAnimation);
 +      } // zoom in from button (factor increases)
 +}
 +
 +void ModalController_hideAll(entity me, float skipAnimation)
 +{
 +      entity e;
 +      for(e = me.firstChild; e; e = e.nextSibling)
 +              me.hideChild(me, e, skipAnimation);
 +}
 +
 +void ModalController_hideChild(entity me, entity other, float skipAnimation)
 +{
 +      if(other.ModalController_state || skipAnimation)
 +      {
 +              me.setFocus(me, NULL);
 +              me.switchState(me, other, 0, skipAnimation);
 +              if(other.ModalController_controllingButton)
 +              {
 +                      other.ModalController_controllingButton.forcePressed = 0;
 +                      other.ModalController_controllingButton = NULL;
 +              }
 +      } // just alpha fade out (factor increases and decreases alpha)
 +}
 +#endif
index 79a294a,0000000..1413039
mode 100644,000000..100644
--- /dev/null
@@@ -1,363 -1,0 +1,367 @@@
 +#ifdef INTERFACE
 +CLASS(Nexposee) EXTENDS(Container)
 +      METHOD(Nexposee, draw, void(entity))
 +      METHOD(Nexposee, keyDown, float(entity, float, float, float))
 +      METHOD(Nexposee, keyUp, float(entity, float, float, float))
 +      METHOD(Nexposee, mousePress, float(entity, vector))
 +      METHOD(Nexposee, mouseMove, float(entity, vector))
 +      METHOD(Nexposee, mouseRelease, float(entity, vector))
 +      METHOD(Nexposee, mouseDrag, float(entity, vector))
 +      METHOD(Nexposee, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(Nexposee, focusEnter, void(entity))
 +      METHOD(Nexposee, close, void(entity))
 +
 +      ATTRIB(Nexposee, animationState, float, -1)
 +      ATTRIB(Nexposee, animationFactor, float, 0)
 +      ATTRIB(Nexposee, selectedChild, entity, NULL)
 +      ATTRIB(Nexposee, mouseFocusedChild, entity, NULL)
 +      METHOD(Nexposee, addItem, void(entity, entity, vector, vector, float))
 +      METHOD(Nexposee, calc, void(entity))
 +      METHOD(Nexposee, setNexposee, void(entity, entity, vector, float, float))
 +      ATTRIB(Nexposee, mousePosition, vector, '0 0 0')
 +      METHOD(Nexposee, pullNexposee, void(entity, entity, vector))
 +ENDCLASS(Nexposee)
 +
 +void ExposeeCloseButton_Click(entity button, entity other); // un-exposees the current state
 +#endif
 +
 +// animation states:
 +//   0 = thumbnails seen
 +//   1 = zooming in
 +//   2 = zoomed in
 +//   3 = zooming out
 +// animation factor: 0 = minimum theSize, 1 = maximum theSize
 +
 +#ifdef IMPLEMENTATION
 +
 +.vector Nexposee_initialSize;
 +.vector Nexposee_initialFontScale;
 +.vector Nexposee_initialOrigin;
 +.float Nexposee_initialAlpha;
 +
 +.vector Nexposee_smallSize;
 +.vector Nexposee_smallOrigin;
 +.float Nexposee_smallAlpha;
 +.float Nexposee_mediumAlpha;
 +.vector Nexposee_scaleCenter;
 +.vector Nexposee_align;
 +.float Nexposee_animationFactor;
 +
 +void Nexposee_close(entity me)
 +{
 +      // user must override this
 +}
 +
 +void ExposeeCloseButton_Click(entity button, entity other)
 +{
 +      other.selectedChild = other.focusedChild;
 +      other.setFocus(other, NULL);
 +      other.animationState = 3;
 +}
 +
 +void Nexposee_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.calc(me);
 +      me.resizeNotifyLie(me, relOrigin, relSize, absOrigin, absSize, Nexposee_initialOrigin, Nexposee_initialSize, Nexposee_initialFontScale);
 +}
 +
 +void Nexposee_Calc_Scale(entity me, float scale)
 +{
 +      entity e;
 +      for(e = me.firstChild; e; e = e.nextSibling)
 +      {
 +              e.Nexposee_smallOrigin = (e.Nexposee_initialOrigin - e.Nexposee_scaleCenter) * scale + e.Nexposee_scaleCenter;
 +              e.Nexposee_smallSize = e.Nexposee_initialSize * scale;
 +              if(e.Nexposee_align.x > 0)
 +                      e.Nexposee_smallOrigin_x = 1 - e.Nexposee_align.x * scale;
 +              if(e.Nexposee_align.x < 0)
 +                      e.Nexposee_smallOrigin_x = -e.Nexposee_smallSize.x + e.Nexposee_align.x * scale;
 +              if(e.Nexposee_align.y > 0)
 +                      e.Nexposee_smallOrigin_y = 1 - e.Nexposee_align.y * scale;
 +              if(e.Nexposee_align.y < 0)
 +                      e.Nexposee_smallOrigin_y = -e.Nexposee_smallSize.y + e.Nexposee_align.y * scale;
 +      }
 +}
 +
 +void Nexposee_calc(entity me)
 +{
 +      /*
 +       * patented by Apple
 +       * can't put that here ;)
 +       */
 +      float scale;
 +      entity e, e2;
 +      vector emins, emaxs, e2mins, e2maxs;
 +
 +      for(scale = 0.7;; scale *= 0.99)
 +      {
 +              Nexposee_Calc_Scale(me, scale);
 +
 +              for(e = me.firstChild; e; e = e.nextSibling)
 +              {
 +                      emins = e.Nexposee_smallOrigin;
 +                      emaxs = emins + e.Nexposee_smallSize;
 +                      for(e2 = e.nextSibling; e2; e2 = e2.nextSibling)
 +                      {
 +                              e2mins = e2.Nexposee_smallOrigin;
 +                              e2maxs = e2mins + e2.Nexposee_smallSize;
 +
 +                              // two intervals [amins, amaxs] and [bmins, bmaxs] overlap if:
 +                              //   amins < bmins < amaxs < bmaxs
 +                              // for which suffices
 +                              //   bmins < amaxs
 +                              //   amins < bmaxs
 +                              if((e2mins.x - emaxs.x) * (emins.x - e2maxs.x) > 0) // x overlap
 +                                      if((e2mins.y - emaxs.y) * (emins.y - e2maxs.y) > 0) // y overlap
 +                                      {
 +                                              goto have_overlap;
 +                                      }
 +                      }
 +              }
 +
 +              break;
 +:have_overlap
 +      }
 +
 +      scale *= 0.95;
 +
 +      Nexposee_Calc_Scale(me, scale);
 +}
 +
 +void Nexposee_setNexposee(entity me, entity other, vector scalecenter, float a0, float a1)
 +{
 +      other.Nexposee_scaleCenter = scalecenter;
 +      other.Nexposee_smallAlpha = a0;
 +      me.setAlphaOf(me, other, a0);
 +      other.Nexposee_mediumAlpha = a1;
 +}
 +
 +void Nexposee_draw(entity me)
 +{
 +      float a;
 +      float a0;
 +      entity e;
 +      float f;
 +      vector fs;
 +
 +      if(me.animationState == -1)
 +      {
 +              me.animationState = 0;
 +      }
 +
 +      f = min(1, frametime * 5);
 +      switch(me.animationState)
 +      {
 +              case 0:
 +                      me.animationFactor = 0;
 +                      break;
 +              case 1:
 +                      me.animationFactor += f;
 +                      if(me.animationFactor >= 1)
 +                      {
 +                              me.animationFactor = 1;
 +                              me.animationState = 2;
 +                              SUPER(Nexposee).setFocus(me, me.selectedChild);
 +                      }
 +                      break;
 +              case 2:
 +                      me.animationFactor = 1;
 +                      break;
 +              case 3:
 +                      me.animationFactor -= f;
 +                      me.mouseFocusedChild = me.itemFromPoint(me, me.mousePosition);
 +                      if(me.animationFactor <= 0)
 +                      {
 +                              me.animationFactor = 0;
 +                              me.animationState = 0;
 +                              me.selectedChild = me.mouseFocusedChild;
 +                      }
 +                      break;
 +      }
 +
 +      f = min(1, frametime * 10);
 +      for(e = me.firstChild; e; e = e.nextSibling)
 +      {
 +              if(e == me.selectedChild)
 +              {
 +                      e.Container_origin = e.Nexposee_smallOrigin * (1 - me.animationFactor) + e.Nexposee_initialOrigin * me.animationFactor;
 +                      e.Container_size = e.Nexposee_smallSize * (1 - me.animationFactor) + e.Nexposee_initialSize * me.animationFactor;
 +                      e.Nexposee_animationFactor = me.animationFactor;
 +                      a0 = e.Nexposee_mediumAlpha;
 +                      if(me.animationState == 3)
 +                              if(e != me.mouseFocusedChild)
 +                                      a0 = e.Nexposee_smallAlpha;
 +                      a = a0 * (1 - me.animationFactor) + me.animationFactor;
 +              }
 +              else
 +              {
 +                      // minimum theSize counts
 +                      e.Container_origin = e.Nexposee_smallOrigin;
 +                      e.Container_size = e.Nexposee_smallSize;
 +                      e.Nexposee_animationFactor = 0;
 +                      a = e.Nexposee_smallAlpha * (1 - me.animationFactor);
 +              }
 +              me.setAlphaOf(me, e, e.Container_alpha * (1 - f) + a * f);
 +
 +              fs = globalToBoxSize(e.Container_size, e.Nexposee_initialSize);
 +              e.Container_fontscale_x = fs.x * e.Nexposee_initialFontScale.x;
 +              e.Container_fontscale_y = fs.y * e.Nexposee_initialFontScale.y;
 +      }
 +
 +      SUPER(Nexposee).draw(me);
 +}
 +
 +float Nexposee_mousePress(entity me, vector pos)
 +{
 +      if(me.animationState == 0)
 +      {
 +              me.mouseFocusedChild = NULL;
 +              Nexposee_mouseMove(me, pos);
 +              if(me.mouseFocusedChild)
 +              {
++                      m_play_click_sound(MENU_SOUND_OPEN);
 +                      me.animationState = 1;
 +                      SUPER(Nexposee).setFocus(me, NULL);
 +              }
 +              else
 +                      me.close(me);
 +              return 1;
 +      }
 +      else if(me.animationState == 2)
 +      {
 +              if (!(SUPER(Nexposee).mousePress(me, pos)))
 +              {
++                      m_play_click_sound(MENU_SOUND_CLOSE);
 +                      me.animationState = 3;
 +                      SUPER(Nexposee).setFocus(me, NULL);
 +              }
 +              return 1;
 +      }
 +      return 0;
 +}
 +
 +float Nexposee_mouseRelease(entity me, vector pos)
 +{
 +      if(me.animationState == 2)
 +              return SUPER(Nexposee).mouseRelease(me, pos);
 +      return 0;
 +}
 +
 +float Nexposee_mouseDrag(entity me, vector pos)
 +{
 +      if(me.animationState == 2)
 +              return SUPER(Nexposee).mouseDrag(me, pos);
 +      return 0;
 +}
 +
 +float Nexposee_mouseMove(entity me, vector pos)
 +{
 +      entity e;
 +      me.mousePosition = pos;
 +      e = me.mouseFocusedChild;
 +      me.mouseFocusedChild = me.itemFromPoint(me, pos);
 +      if(me.animationState == 2)
 +              return SUPER(Nexposee).mouseMove(me, pos);
 +      if(me.animationState == 0)
 +      {
 +              if(me.mouseFocusedChild)
 +                      if(me.mouseFocusedChild != e || me.mouseFocusedChild != me.selectedChild)
 +                              me.selectedChild = me.mouseFocusedChild;
 +              return 1;
 +      }
 +      return 0;
 +}
 +
 +float Nexposee_keyUp(entity me, float scan, float ascii, float shift)
 +{
 +      if(me.animationState == 2)
 +              return SUPER(Nexposee).keyUp(me, scan, ascii, shift);
 +      return 0;
 +}
 +
 +float Nexposee_keyDown(entity me, float scan, float ascii, float shift)
 +{
 +      float nexposeeKey = 0;
 +      if(me.animationState == 2)
 +              if(SUPER(Nexposee).keyDown(me, scan, ascii, shift))
 +                      return 1;
 +      if(scan == K_TAB)
 +      {
 +              if(me.animationState == 0)
 +              {
 +                      if(shift & S_SHIFT)
 +                      {
 +                              if(me.selectedChild)
 +                                      me.selectedChild = me.selectedChild.prevSibling;
 +                              if (!me.selectedChild)
 +                                      me.selectedChild = me.lastChild;
 +                      }
 +                      else
 +                      {
 +                              if(me.selectedChild)
 +                                      me.selectedChild = me.selectedChild.nextSibling;
 +                              if (!me.selectedChild)
 +                                      me.selectedChild = me.firstChild;
 +                      }
 +              }
 +      }
 +      switch(me.animationState)
 +      {
 +              default:
 +              case 0:
 +              case 3:
 +                      nexposeeKey = ((scan == K_SPACE) || (scan == K_ENTER) || (scan == K_KP_ENTER));
 +                      break;
 +              case 1:
 +              case 2:
 +                      nexposeeKey = (scan == K_ESCAPE);
 +                      break;
 +      }
 +      if(nexposeeKey)
 +      {
 +              switch(me.animationState)
 +              {
 +                      default:
 +                      case 0:
 +                      case 3:
++                              m_play_click_sound(MENU_SOUND_OPEN);
 +                              me.animationState = 1;
 +                              break;
 +                      case 1:
 +                      case 2:
++                              m_play_click_sound(MENU_SOUND_CLOSE);
 +                              me.animationState = 3;
 +                              break;
 +              }
 +              if(me.focusedChild)
 +                      me.selectedChild = me.focusedChild;
 +              if (!me.selectedChild)
 +                      me.animationState = 0;
 +              SUPER(Nexposee).setFocus(me, NULL);
 +              return 1;
 +      }
 +      return 0;
 +}
 +
 +void Nexposee_addItem(entity me, entity other, vector theOrigin, vector theSize, float theAlpha)
 +{
 +      SUPER(Nexposee).addItem(me, other, theOrigin, theSize, theAlpha);
 +      other.Nexposee_initialFontScale = other.Container_fontscale;
 +      other.Nexposee_initialSize = other.Container_size;
 +      other.Nexposee_initialOrigin = other.Container_origin;
 +      other.Nexposee_initialAlpha = other.Container_alpha;
 +      if(other.Nexposee_initialFontScale == '0 0 0')
 +              other.Nexposee_initialFontScale = '1 1 0';
 +}
 +
 +void Nexposee_focusEnter(entity me)
 +{
 +      if(me.animationState == 2)
 +              SUPER(Nexposee).setFocus(me, me.selectedChild);
 +}
 +
 +void Nexposee_pullNexposee(entity me, entity other, vector theAlign)
 +{
 +      other.Nexposee_align = theAlign;
 +}
 +#endif
index f97871b,0000000..2c74f61
mode 100644,000000..100644
--- /dev/null
@@@ -1,285 -1,0 +1,301 @@@
-       METHOD(Slider, focusEnter, void(entity))
 +// Note:
 +//   to use this, you FIRST call configureSliderVisuals, then configureSliderValues
 +#ifdef INTERFACE
 +CLASS(Slider) EXTENDS(Label)
 +      METHOD(Slider, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(Slider, configureSliderVisuals, void(entity, float, float, float, string))
 +      METHOD(Slider, configureSliderValues, void(entity, float, float, float, float, float, float))
 +      METHOD(Slider, draw, void(entity))
 +      METHOD(Slider, keyDown, float(entity, float, float, float))
++      METHOD(Slider, keyUp, float(entity, float, float, float))
 +      METHOD(Slider, mousePress, float(entity, vector))
 +      METHOD(Slider, mouseDrag, float(entity, vector))
 +      METHOD(Slider, mouseRelease, float(entity, vector))
-       // TODO more keys
 +      METHOD(Slider, valueToText, string(entity, float))
 +      METHOD(Slider, toString, string(entity))
 +      METHOD(Slider, setValue, void(entity, float))
 +      METHOD(Slider, setSliderValue, void(entity, float))
 +      METHOD(Slider, showNotify, void(entity))
 +      ATTRIB(Slider, src, string, string_null)
 +      ATTRIB(Slider, focusable, float, 1)
++      ATTRIB(Slider, allowFocusSound, float, 1)
 +      ATTRIB(Slider, value, float, 0)
 +      ATTRIB(Slider, animated, float, 1)
 +      ATTRIB(Slider, sliderValue, float, 0)
 +      ATTRIB(Slider, valueMin, float, 0)
 +      ATTRIB(Slider, valueMax, float, 0)
 +      ATTRIB(Slider, valueStep, float, 0)
 +      ATTRIB(Slider, valueDigits, float, 0)
 +      ATTRIB(Slider, valueKeyStep, float, 0)
 +      ATTRIB(Slider, valuePageStep, float, 0)
 +      ATTRIB(Slider, valueDisplayMultiplier, float, 1.0)
 +      ATTRIB(Slider, textSpace, float, 0)
 +      ATTRIB(Slider, controlWidth, float, 0)
 +      ATTRIB(Slider, pressed, float, 0)
 +      ATTRIB(Slider, pressOffset, float, 0)
 +      ATTRIB(Slider, previousValue, float, 0)
 +      ATTRIB(Slider, tolerance, vector, '0 0 0')
 +      ATTRIB(Slider, disabled, float, 0)
 +      ATTRIB(Slider, color, vector, '1 1 1')
 +      ATTRIB(Slider, color2, vector, '1 1 1')
 +      ATTRIB(Slider, colorD, vector, '1 1 1')
 +      ATTRIB(Slider, colorC, vector, '1 1 1')
 +      ATTRIB(Slider, colorF, vector, '1 1 1')
 +      ATTRIB(Slider, disabledAlpha, float, 0.3)
 +ENDCLASS(Slider)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void Slider_setValue(entity me, float val)
 +{
 +      if (me.animated) {
 +              anim.removeObjAnim(anim, me);
 +              makeHostedEasing(me, Slider_setSliderValue, easingQuadInOut, 1, me.sliderValue, val);
 +      } else {
 +              me.setSliderValue(me, val);
 +      }
 +      me.value = val;
 +}
 +void Slider_setSliderValue(entity me, float val)
 +{
 +      me.sliderValue = val;
 +}
 +string Slider_toString(entity me)
 +{
 +      return sprintf("%d (%s)", me.value, me.valueToText(me, me.value));
 +}
 +void Slider_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      SUPER(Slider).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +      me.controlWidth = absSize.y / absSize.x;
 +}
 +string Slider_valueToText(entity me, float val)
 +{
 +      if(almost_in_bounds(me.valueMin, val, me.valueMax))
 +              return ftos_decimals(val * me.valueDisplayMultiplier, me.valueDigits);
 +      return "";
 +}
 +void Slider_configureSliderVisuals(entity me, float sz, float theAlign, float theTextSpace, string gfx)
 +{
 +      SUPER(Slider).configureLabel(me, string_null, sz, theAlign);
 +      me.textSpace = theTextSpace;
 +      me.keepspaceLeft = (theTextSpace == 0) ? 0 : (1 - theTextSpace);
 +      me.src = gfx;
 +}
 +void Slider_configureSliderValues(entity me, float theValueMin, float theValue, float theValueMax, float theValueStep, float theValueKeyStep, float theValuePageStep)
 +{
 +      me.value = theValue;
 +      me.sliderValue = theValue;
 +      me.valueStep = theValueStep;
 +      me.valueMin = theValueMin;
 +      me.valueMax = theValueMax;
 +      me.valueKeyStep = theValueKeyStep;
 +      me.valuePageStep = theValuePageStep;
 +      me.valueDigits = 3;
 +      if(fabs(floor(me.valueStep * 100 + 0.5) - (me.valueStep * 100)) < 0.01) // about a whole number of 100ths
 +              me.valueDigits = 2;
 +      if(fabs(floor(me.valueStep * 10 + 0.5) - (me.valueStep * 10)) < 0.01) // about a whole number of 10ths
 +              me.valueDigits = 1;
 +      if(fabs(floor(me.valueStep * 1 + 0.5) - (me.valueStep * 1)) < 0.01) // about a whole number
 +              me.valueDigits = 0;
 +}
 +float Slider_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      float inRange;
 +      if(me.disabled)
 +              return 0;
 +      inRange = (almost_in_bounds(me.valueMin, me.value, me.valueMax));
 +      if(key == K_LEFTARROW || key == K_KP_LEFTARROW || key == K_MWHEELDOWN)
 +      {
 +              if(inRange)
 +                      me.setValue(me, median(me.valueMin, me.value - me.valueKeyStep, me.valueMax));
 +              else
 +                      me.setValue(me, me.valueMax);
 +              return 1;
 +      }
 +      if(key == K_RIGHTARROW || key == K_KP_RIGHTARROW || key == K_MWHEELUP)
 +      {
 +              if(inRange)
 +                      me.setValue(me, median(me.valueMin, me.value + me.valueKeyStep, me.valueMax));
 +              else
 +                      me.setValue(me, me.valueMin);
 +              return 1;
 +      }
 +      if(key == K_PGDN || key == K_KP_PGDN)
 +      {
 +              if(inRange)
 +                      me.setValue(me, median(me.valueMin, me.value - me.valuePageStep, me.valueMax));
 +              else
 +                      me.setValue(me, me.valueMax);
 +              return 1;
 +      }
 +      if(key == K_PGUP || key == K_KP_PGUP)
 +      {
 +              if(inRange)
 +                      me.setValue(me, median(me.valueMin, me.value + me.valuePageStep, me.valueMax));
 +              else
 +                      me.setValue(me, me.valueMin);
 +              return 1;
 +      }
 +      if(key == K_HOME || key == K_KP_HOME)
 +      {
 +              me.setValue(me, me.valueMin);
 +              return 1;
 +      }
 +      if(key == K_END || key == K_KP_END)
 +      {
 +              me.setValue(me, me.valueMax);
 +              return 1;
 +      }
-       if(cvar("menu_sounds"))
-               localsound("sound/misc/menu2.wav");
++      // TODO more keys (NOTE also add them to Slider_keyUp)
++      return 0;
++}
++float Slider_keyUp(entity me, float key, float ascii, float shift)
++{
++      if(me.disabled)
++              return 0;
++      switch(key)
++      {
++              case K_LEFTARROW:
++              case K_KP_LEFTARROW:
++              case K_RIGHTARROW:
++              case K_KP_RIGHTARROW:
++              case K_PGUP:
++              case K_KP_PGUP:
++              case K_PGDN:
++              case K_KP_PGDN:
++              case K_HOME:
++              case K_KP_HOME:
++              case K_END:
++              case K_KP_END:
++                      m_play_click_sound(MENU_SOUND_SLIDE);
++      }
 +      return 0;
 +}
 +float Slider_mouseDrag(entity me, vector pos)
 +{
 +      float hit;
 +      float v, animed;
 +      if(me.disabled)
 +              return 0;
 +
 +      anim.removeObjAnim(anim, me);
 +      animed = me.animated;
 +      me.animated = false;
 +
 +      if(me.pressed)
 +      {
 +              hit = 1;
 +              if(pos.x < 0 - me.tolerance.x) hit = 0;
 +              if(pos.y < 0 - me.tolerance.y) hit = 0;
 +              if(pos.x >= 1 - me.textSpace + me.tolerance.x) hit = 0;
 +              if(pos.y >= 1 + me.tolerance.y) hit = 0;
 +              if(hit)
 +              {
 +                      v = median(0, (pos.x - me.pressOffset - 0.5 * me.controlWidth) / (1 - me.textSpace - me.controlWidth), 1) * (me.valueMax - me.valueMin) + me.valueMin;
 +                      if(me.valueStep)
 +                              v = floor(0.5 + v / me.valueStep) * me.valueStep;
 +                      me.setValue(me, v);
 +              }
 +              else
 +                      me.setValue(me, me.previousValue);
 +      }
 +
 +      me.animated = animed;
 +
 +      return 1;
 +}
 +float Slider_mousePress(entity me, vector pos)
 +{
 +      float controlCenter;
 +      if(me.disabled)
 +              return 0;
 +      if(pos.x < 0) return 0;
 +      if(pos.y < 0) return 0;
 +      if(pos.x >= 1 - me.textSpace) return 0;
 +      if(pos.y >= 1) return 0;
 +      controlCenter = (me.value - me.valueMin) / (me.valueMax - me.valueMin) * (1 - me.textSpace - me.controlWidth) + 0.5 * me.controlWidth;
 +      if(fabs(pos.x - controlCenter) <= 0.5 * me.controlWidth)
 +      {
 +              me.pressed = 1;
 +              me.pressOffset = pos.x - controlCenter;
 +              me.previousValue = me.value;
 +              //me.mouseDrag(me, pos);
 +      }
 +      else
 +      {
 +              float clickValue, pageValue, inRange;
 +              clickValue = median(0, (pos.x - me.pressOffset - 0.5 * me.controlWidth) / (1 - me.textSpace - me.controlWidth), 1) * (me.valueMax - me.valueMin) + me.valueMin;
 +              inRange = (almost_in_bounds(me.valueMin, me.value, me.valueMax));
 +              if(pos.x < controlCenter)
 +              {
 +                      pageValue = me.value - me.valuePageStep;
 +                      if(me.valueStep)
 +                              clickValue = floor(clickValue / me.valueStep) * me.valueStep;
 +                      pageValue = max(pageValue, clickValue);
 +                      if(inRange)
 +                              me.setValue(me, median(me.valueMin, pageValue, me.valueMax));
 +                      else
 +                              me.setValue(me, me.valueMax);
 +              }
 +              else
 +              {
 +                      pageValue = me.value + me.valuePageStep;
 +                      if(me.valueStep)
 +                              clickValue = ceil(clickValue / me.valueStep) * me.valueStep;
 +                      pageValue = min(pageValue, clickValue);
 +                      if(inRange)
 +                              me.setValue(me, median(me.valueMin, pageValue, me.valueMax));
 +                      else
 +                              me.setValue(me, me.valueMax);
 +              }
 +              if(pageValue == clickValue)
 +              {
 +                      controlCenter = (me.value - me.valueMin) / (me.valueMax - me.valueMin) * (1 - me.textSpace - me.controlWidth) + 0.5 * me.controlWidth;
 +                      me.pressed = 1;
 +                      me.pressOffset = pos.x - controlCenter;
 +                      me.previousValue = me.value;
 +                      //me.mouseDrag(me, pos);
 +              }
 +      }
 +      return 1;
 +}
 +float Slider_mouseRelease(entity me, vector pos)
 +{
 +      me.pressed = 0;
 +      if(me.disabled)
 +              return 0;
- void Slider_focusEnter(entity me)
- {
-       if(cvar("menu_sounds") > 1)
-               localsound("sound/misc/menu1.wav");
-       SUPER(Slider).focusEnter(me);
- }
++      m_play_click_sound(MENU_SOUND_SLIDE);
 +      return 1;
 +}
 +void Slider_showNotify(entity me)
 +{
 +      me.focusable = !me.disabled;
 +}
 +void Slider_draw(entity me)
 +{
 +      float controlLeft;
 +      float save;
 +      me.focusable = !me.disabled;
 +      save = draw_alpha;
 +      if(me.disabled)
 +              draw_alpha *= me.disabledAlpha;
 +      draw_ButtonPicture('0 0 0', strcat(me.src, "_s"), eX * (1 - me.textSpace) + eY, me.color2, 1);
 +      if(almost_in_bounds(me.valueMin, me.sliderValue, me.valueMax))
 +      {
 +              controlLeft = (me.sliderValue - me.valueMin) / (me.valueMax - me.valueMin) * (1 - me.textSpace - me.controlWidth);
 +              if(me.disabled)
 +                      draw_Picture(eX * controlLeft, strcat(me.src, "_d"), eX * me.controlWidth + eY, me.colorD, 1);
 +              else if(me.pressed)
 +                      draw_Picture(eX * controlLeft, strcat(me.src, "_c"), eX * me.controlWidth + eY, me.colorC, 1);
 +              else if(me.focused)
 +                      draw_Picture(eX * controlLeft, strcat(me.src, "_f"), eX * me.controlWidth + eY, me.colorF, 1);
 +              else
 +                      draw_Picture(eX * controlLeft, strcat(me.src, "_n"), eX * me.controlWidth + eY, me.color, 1);
 +      }
 +      me.setText(me, me.valueToText(me, me.value));
 +      draw_alpha = save;
 +      SUPER(Slider).draw(me);
 +      me.text = string_null; // TEMPSTRING!
 +}
 +#endif
Simple merge
@@@ -52,4 -38,17 +52,18 @@@ void preMenuDraw(); // this is run befo
  void postMenuDraw(); // this is run just after the menu is drawn (or not). Useful to draw something over everything else.
  
  void m_sync();
+ // sounds
+ const string MENU_SOUND_CLEAR   = "sound/menu/clear.wav";
+ const string MENU_SOUND_CLOSE   = "sound/menu/close.wav";
+ const string MENU_SOUND_EXECUTE = "sound/menu/execute.wav";
+ const string MENU_SOUND_FOCUS   = "sound/menu/focus.wav";
+ const string MENU_SOUND_OPEN    = "sound/menu/open.wav";
+ const string MENU_SOUND_SELECT  = "sound/menu/select.wav";
+ const string MENU_SOUND_SLIDE   = "sound/menu/slide.wav";
+ const string MENU_SOUND_WINNER  = "sound/menu/winner.wav";
+ void m_play_focus_sound();
+ void m_play_click_sound(string soundfile);
 +#endif
index 6634728,0000000..f16ab0e
mode 100644,000000..100644
--- /dev/null
@@@ -1,174 -1,0 +1,175 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticColorpicker) EXTENDS(Image)
 +      METHOD(XonoticColorpicker, configureXonoticColorpicker, void(entity, entity))
 +      METHOD(XonoticColorpicker, mousePress, float(entity, vector))
 +      METHOD(XonoticColorpicker, mouseRelease, float(entity, vector))
 +      METHOD(XonoticColorpicker, mouseDrag, float(entity, vector))
 +      ATTRIB(XonoticColorpicker, controlledTextbox, entity, NULL)
 +      ATTRIB(XonoticColorpicker, image, string, SKINGFX_COLORPICKER)
 +      ATTRIB(XonoticColorpicker, imagemargin, vector, SKINMARGIN_COLORPICKER)
 +      ATTRIB(XonoticColorpicker, focusable, float, 1)
 +      METHOD(XonoticColorpicker, focusLeave, void(entity))
 +      METHOD(XonoticColorpicker, keyDown, float(entity, float, float, float))
 +      METHOD(XonoticColorpicker, draw, void(entity))
 +ENDCLASS(XonoticColorpicker)
 +entity makeXonoticColorpicker(entity theTextbox);
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +entity makeXonoticColorpicker(entity theTextbox)
 +{
 +      entity me;
 +      me = spawnXonoticColorpicker();
 +      me.configureXonoticColorpicker(me, theTextbox);
 +      return me;
 +}
 +
 +void XonoticColorpicker_configureXonoticColorpicker(entity me, entity theTextbox)
 +{
 +      me.controlledTextbox = theTextbox;
 +      me.configureImage(me, me.image);
 +}
 +
 +float XonoticColorpicker_mousePress(entity me, vector coords)
 +{
 +      me.mouseDrag(me, coords);
 +      return 1;
 +}
 +
 +// must match hslimage.c
 +vector hslimage_color(vector v, vector margin)
 +{
 +    v_x = (v.x - margin.x) / (1 - 2 * margin.x);
 +    v_y = (v.y - margin.y) / (1 - 2 * margin.y);
 +    if(v.x < 0) v_x = 0;
 +    if(v.y < 0) v_y = 0;
 +    if(v.x > 1) v_x = 1;
 +    if(v.y > 1) v_y = 1;
 +    if(v.y > 0.875) // grey bar
 +        return hsl_to_rgb(eZ * v.x);
 +    else
 +        return hsl_to_rgb(v.x * 6 * eX + eY + v.y / 0.875 * eZ);
 +}
 +
 +vector color_hslimage(vector v, vector margin)
 +{
 +      vector pos = '0 0 0';
 +      v = rgb_to_hsl(v);
 +      if (v.y)
 +      {
 +              pos_x = v.x / 6;
 +              pos_y = v.z * 0.875;
 +      }
 +      else // grey scale
 +      {
 +              pos_x = v.z;
 +              pos_y = 0.875 + 0.07;
 +      }
 +      pos_x = margin.x + pos.x * (1 - 2 * margin.x);
 +      pos_y = margin.y + pos.y * (1 - 2 * margin.y);
 +      return pos;
 +}
 +
 +float XonoticColorpicker_mouseDrag(entity me, vector coords)
 +{
 +      float i, carets;
 +      for (;;)
 +      {
 +              i = me.controlledTextbox.cursorPos;
 +              if(i >= 2)
 +              {
 +                      if(substring(me.controlledTextbox.text, i-2, 1) == "^")
 +                      {
 +                              carets = 1;
 +                              while (i - 2 - carets >= 0 && substring(me.controlledTextbox.text, i - 2 - carets, 1) == "^")
 +                                      ++carets;
 +                              if (carets & 1)
 +                                      if(strstrofs("0123456789", substring(me.controlledTextbox.text, i-1, 1), 0) >= 0)
 +                                      {
 +                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                              continue;
 +                                      }
 +                      }
 +              }
 +
 +              if(i >= 5)
 +              {
 +                      if(substring(me.controlledTextbox.text, i-5, 2) == "^x")
 +                      {
 +                              carets = 1;
 +                              while (i - 5 - carets >= 0 && substring(me.controlledTextbox.text, i - 5 - carets, 1) == "^")
 +                                      ++carets;
 +                              if (carets & 1)
 +                                      if(strstrofs("0123456789abcdefABCDEF", substring(me.controlledTextbox.text, i-3, 1), 0) >= 0)
 +                                              if(strstrofs("0123456789abcdefABCDEF", substring(me.controlledTextbox.text, i-2, 1), 0) >= 0)
 +                                                      if(strstrofs("0123456789abcdefABCDEF", substring(me.controlledTextbox.text, i-1, 1), 0) >= 0)
 +                                                      {
 +                                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                                              me.controlledTextbox.keyDown(me.controlledTextbox, K_BACKSPACE, 8, 0);
 +                                                              continue;
 +                                                      }
 +                      }
 +              }
 +              break;
 +      }
 +
 +      if(substring(me.controlledTextbox.text, i-1, 1) == "^")
 +      {
 +              carets = 1;
 +              while (i - 1 - carets >= 0 && substring(me.controlledTextbox.text, i - 1 - carets, 1) == "^")
 +                      ++carets;
 +              if (carets & 1)
 +                      me.controlledTextbox.enterText(me.controlledTextbox, "^"); // escape previous caret
 +      }
 +
 +      vector margin;
 +      margin = me.imagemargin;
 +      if(coords.x >= margin.x)
 +      if(coords.y >= margin.y)
 +      if(coords.x <= 1 - margin.x)
 +      if(coords.y <= 1 - margin.y)
 +              me.controlledTextbox.enterText(me.controlledTextbox, rgb_to_hexcolor(hslimage_color(coords, margin)));
 +
 +      return 1;
 +}
 +
 +float XonoticColorpicker_mouseRelease(entity me, vector coords)
 +{
++      m_play_click_sound(MENU_SOUND_SLIDE);
 +      me.mouseDrag(me, coords);
 +      return 1;
 +}
 +
 +void XonoticColorpicker_focusLeave(entity me)
 +{
 +      me.controlledTextbox.saveCvars(me.controlledTextbox);
 +}
 +float XonoticColorpicker_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      return me.controlledTextbox.keyDown(me.controlledTextbox, key, ascii, shift);
 +}
 +void XonoticColorpicker_draw(entity me)
 +{
 +      SUPER(XonoticColorpicker).draw(me);
 +
 +      float B, C, aC;
 +      C = cvar("r_textcontrast");
 +      B = cvar("r_textbrightness");
 +
 +      // for this to work, C/(1-B) must be in 0..1
 +      // B must be < 1
 +      // C must be < 1-B
 +
 +      B = bound(0, B, 1);
 +      C = bound(0, C, 1-B);
 +
 +      aC = 1 - C / (1 - B);
 +
 +      draw_Picture(me.imgOrigin, strcat(me.src, "_m"), me.imgSize, '0 0 0', aC);
 +      draw_Picture(me.imgOrigin, strcat(me.src, "_m"), me.imgSize, me.color, B);
 +}
 +#endif
index f0e3e6d,0000000..458a72c
mode 100644,000000..100644
--- /dev/null
@@@ -1,122 -1,0 +1,123 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticColorpickerString) EXTENDS(Image)
 +      METHOD(XonoticColorpickerString, configureXonoticColorpickerString, void(entity, string, string))
 +      METHOD(XonoticColorpickerString, mousePress, float(entity, vector))
 +      METHOD(XonoticColorpickerString, mouseRelease, float(entity, vector))
 +      METHOD(XonoticColorpickerString, mouseDrag, float(entity, vector))
 +      ATTRIB(XonoticColorpickerString, cvarName, string, string_null)
 +      METHOD(XonoticColorPickerString, loadCvars, void(entity))
 +      METHOD(XonoticColorPickerString, saveCvars, void(entity))
 +      ATTRIB(XonoticColorpickerString, prevcoords, vector, '0 0 0')
 +      ATTRIB(XonoticColorpickerString, image, string, SKINGFX_COLORPICKER)
 +      ATTRIB(XonoticColorpickerString, imagemargin, vector, SKINMARGIN_COLORPICKER)
 +      ATTRIB(XonoticColorpickerString, focusable, float, 1)
 +      METHOD(XonoticColorpickerString, draw, void(entity))
 +      ATTRIB(XonoticColorpickerString, disabledAlpha, float, 0.3)
 +ENDCLASS(XonoticColorpickerString)
 +entity makeXonoticColorpickerString(string theCvar, string theDefaultCvar);
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +entity makeXonoticColorpickerString(string theCvar, string theDefaultCvar)
 +{
 +      entity me;
 +      me = spawnXonoticColorpickerString();
 +      me.configureXonoticColorpickerString(me, theCvar, theDefaultCvar);
 +      return me;
 +}
 +
 +void XonoticColorpickerString_configureXonoticColorpickerString(entity me, string theCvar, string theDefaultCvar)
 +{
 +      me.cvarName = theCvar;
 +      me.configureImage(me, me.image);
 +      if(theCvar)
 +      {
 +              me.cvarName = theCvar;
 +              me.tooltip = getZonedTooltipForIdentifier(theCvar);
 +              me.loadCvars(me);
 +      }
 +}
 +
 +void XonoticColorPickerString_loadCvars(entity me)
 +{
 +      if (!me.cvarName)
 +              return;
 +
 +      if(substring(me.cvarName, -1, 1) == "_")
 +      {
 +              me.prevcoords = color_hslimage(
 +                      eX * cvar(strcat(me.cvarName, "red")) +
 +                      eY * cvar(strcat(me.cvarName, "green")) +
 +                      eZ * cvar(strcat(me.cvarName, "blue")),
 +                      me.imagemargin);
 +      }
 +      else
 +              me.prevcoords = color_hslimage(stov(cvar_string(me.cvarName)), me.imagemargin);
 +}
 +
 +void XonoticColorPickerString_saveCvars(entity me)
 +{
 +      if (!me.cvarName)
 +              return;
 +
 +      if(substring(me.cvarName, -1, 1) == "_")
 +      {
 +              vector v = hslimage_color(me.prevcoords, me.imagemargin);
 +              cvar_set(strcat(me.cvarName, "red"), ftos(v.x));
 +              cvar_set(strcat(me.cvarName, "green"), ftos(v.y));
 +              cvar_set(strcat(me.cvarName, "blue"), ftos(v.z));
 +      }
 +      else
 +              cvar_set(me.cvarName, sprintf("%v", hslimage_color(me.prevcoords, me.imagemargin)));
 +}
 +
 +float XonoticColorpickerString_mousePress(entity me, vector coords)
 +{
 +      me.mouseDrag(me, coords);
 +      return 1;
 +}
 +
 +float XonoticColorpickerString_mouseDrag(entity me, vector coords)
 +{
 +      if(me.disabled)
 +              return 0;
 +      vector margin;
 +      margin = me.imagemargin;
 +      if(coords.x >= margin.x)
 +      if(coords.y >= margin.y)
 +      if(coords.x <= 1 - margin.x)
 +      if(coords.y <= 1 - margin.y)
 +      {
 +              me.prevcoords = coords;
 +              me.saveCvars(me);
 +      }
 +
 +      return 1;
 +}
 +
 +float XonoticColorpickerString_mouseRelease(entity me, vector coords)
 +{
++      m_play_click_sound(MENU_SOUND_SLIDE);
 +      me.mouseDrag(me, coords);
 +      return 1;
 +}
 +
 +void XonoticColorpickerString_draw(entity me)
 +{
 +      float save;
 +      save = draw_alpha;
 +      if(me.disabled)
 +              draw_alpha *= me.disabledAlpha;
 +
 +      SUPER(XonoticColorpickerString).draw(me);
 +
 +      vector sz;
 +      sz = draw_PictureSize(strcat(me.src, "_selected"));
 +      sz = globalToBoxSize(sz, draw_scale);
 +
 +      if(!me.disabled)
 +              draw_Picture(me.imgOrigin + me.prevcoords - 0.5 * sz, strcat(me.src, "_selected"), sz, '1 1 1', 1);
 +
 +      draw_alpha = save;
 +}
 +#endif
index bf891ac,0000000..1adfb01
mode 100644,000000..100644
--- /dev/null
@@@ -1,164 -1,0 +1,164 @@@
-               me.TD(me, 1, 2, e = makeXonoticTextSlider("cl_gender"));
 +#ifdef INTERFACE
 +CLASS(XonoticProfileTab) EXTENDS(XonoticTab)
 +      METHOD(XonoticProfileTab, fill, void(entity))
 +      METHOD(XonoticProfileTab, draw, void(entity))
 +      ATTRIB(XonoticProfileTab, title, string, _("Profile"))
 +      ATTRIB(XonoticProfileTab, intendedWidth, float, 0.9)
 +      ATTRIB(XonoticProfileTab, rows, float, 23)
 +      ATTRIB(XonoticProfileTab, columns, float, 6.1) // added extra .2 for center space
 +      ATTRIB(XonoticProfileTab, playerNameLabel, entity, NULL)
 +      ATTRIB(XonoticProfileTab, playerNameLabelAlpha, float, SKINALPHA_HEADER)
 +ENDCLASS(XonoticProfileTab)
 +entity makeXonoticProfileTab();
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +entity makeXonoticProfileTab()
 +{
 +      entity me;
 +      me = spawnXonoticProfileTab();
 +      me.configureDialog(me);
 +      return me;
 +}
 +void XonoticProfileTab_draw(entity me)
 +{
 +      if(cvar_string("_cl_name") == "Player")
 +              me.playerNameLabel.alpha = ((mod(time * 2, 2) < 1) ? 1 : 0);
 +      else
 +              me.playerNameLabel.alpha = me.playerNameLabelAlpha;
 +      SUPER(XonoticProfileTab).draw(me);
 +}
 +void XonoticProfileTab_fill(entity me)
 +{
 +      entity e, pms, label, box;
 +      float i;
 +
 +      // ==============
 +      //  NAME SECTION
 +      // ==============
 +      me.gotoRC(me, 0.5, 0);
 +              me.TD(me, 1, 3, me.playerNameLabel = makeXonoticHeaderLabel(_("Name")));
 +
 +      me.gotoRC(me, 1.5, 0);
 +              me.TD(me, 1, 3, label = makeXonoticTextLabel(0.5, string_null));
 +                      label.allowCut = 1;
 +                      label.allowColors = 1;
 +                      label.alpha = 1;
 +                      label.isBold = true;
 +                      label.fontSize = SKINFONTSIZE_TITLE;
 +
 +      me.gotoRC(me, 2.5, 0);
 +              me.TD(me, 1, 3.0, box = makeXonoticInputBox(1, "_cl_name"));
 +                      box.forbiddenCharacters = "\r\n\\\"$"; // don't care, isn't getting saved
 +                      box.maxLength = -127; // negative means encoded length in bytes
 +                      box.saveImmediately = 1;
 +                      box.enableClearButton = 0;
 +                      label.textEntity = box;
 +      me.TR(me);
 +              me.TD(me, 5, 1, e = makeXonoticColorpicker(box));
 +              me.TD(me, 5, 2, e = makeXonoticCharmap(box));
 +
 +      // ===============
 +      //  MODEL SECTION
 +      // ===============
 +      //me.gotoRC(me, 0.5, 3.1); me.setFirstColumn(me, me.currentColumn); // TOP RIGHT
 +      //me.gotoRC(me, 9, 3.1); me.setFirstColumn(me, me.currentColumn); // BOTTOM RIGHT
 +      me.gotoRC(me, 9, 0); me.setFirstColumn(me, me.currentColumn); // BOTTOM LEFT
 +              me.TD(me, 1, 3, e = makeXonoticHeaderLabel(_("Model")));
 +
 +      me.TR(me);
 +              //me.TDempty(me, 0); // MODEL LEFT, COLOR RIGHT
 +              me.TDempty(me, 1); // MODEL RIGHT, COLOR LEFT
 +              pms = makeXonoticPlayerModelSelector();
 +              me.TD(me, 1, 0.3, e = makeXonoticButton("<<", '0 0 0'));
 +                      e.onClick = PlayerModelSelector_Prev_Click;
 +                      e.onClickEntity = pms;
 +              me.TD(me, 11.5, 1.4, pms);
 +              me.TD(me, 1, 0.3, e = makeXonoticButton(">>", '0 0 0'));
 +                      e.onClick = PlayerModelSelector_Next_Click;
 +                      e.onClickEntity = pms;
 +
 +      //me.setFirstColumn(me, me.currentColumn + 2); // MODEL LEFT, COLOR RIGHT
 +      me.gotoRC(me, me.currentRow, 0); me.setFirstColumn(me, me.currentColumn); // MODEL RIGHT, COLOR LEFT
 +      me.TR(me);
 +              me.TD(me, 1, 1, e = makeXonoticHeaderLabel(_("Glowing color")));
 +              for(i = 0; i < 15; ++i)
 +              {
 +                      if(mod(i, 5) == 0)
 +                              me.TR(me);
 +                      me.TDNoMargin(me, 1, 0.2, e = makeXonoticColorButton(1, 0, i), '0 1 0');
 +              }
 +      me.TR(me);
 +      me.TR(me);
 +              me.TD(me, 1, 1, e = makeXonoticHeaderLabel(_("Detail color")));
 +              for(i = 0; i < 15; ++i)
 +              {
 +                      if(mod(i, 5) == 0)
 +                              me.TR(me);
 +                      me.TDNoMargin(me, 1, 0.2, e = makeXonoticColorButton(2, 1, i), '0 1 0');
 +              }
 +
 +      // ====================
 +      //  STATISTICS SECTION
 +      // ====================
 +      me.gotoRC(me, 0.5, 3.1); me.setFirstColumn(me, me.currentColumn); // TOP RIGHT
 +      //me.gotoRC(me, 9, 3.1); me.setFirstColumn(me, me.currentColumn); // BOTTOM RIGHT
 +      //me.gotoRC(me, 9, 0); me.setFirstColumn(me, me.currentColumn); // BOTTOM LEFT
 +              me.TD(me, 1, 3, e = makeXonoticHeaderLabel(_("Statistics")));
 +
 +      me.TR(me);
 +              me.TDempty(me, 0.25);
 +              me.TD(me, 1, 2.5, e = makeXonoticCheckBox(0, "cl_allow_uidtracking", _("Allow player statistics to track your client")));
 +      me.TR(me);
 +              me.TDempty(me, 0.25);
 +              me.TD(me, 1, 2.5, e = makeXonoticCheckBox(0, "cl_allow_uid2name", _("Allow player statistics to use your nickname")));
 +              setDependent(e, "cl_allow_uidtracking", 1, 1);
 +      me.gotoRC(me, 4, 3.1); // TOP RIGHT
 +      //me.gotoRC(me, 12.5, 3.1); // BOTTOM RIGHT
 +      //me.gotoRC(me, 12.5, 0); // BOTTOM LEFT
 +              me.TDempty(me, 0.25);
 +              me.TD(me, 9, 2.5, statslist = makeXonoticStatsList());
 +              //setDependent(statslist, "cl_allow_uidtracking", 1, 1);
 +
 +      // =================
 +      //  COUNTRY SECTION
 +      // =================
 +      me.gotoRC(me, 16, 3.1); me.setFirstColumn(me, me.currentColumn); // BOTTOM SECTION, TOP POS
 +      //me.gotoRC(me, 13.5, 3.1); me.setFirstColumn(me, me.currentColumn); // BOTTOM SECTION, TOP POS
 +      //me.gotoRC(me, 0.5, 3.1); me.setFirstColumn(me, me.currentColumn); // TOP SECTION, TOP POS
 +              me.TD(me, 1, 3, e = makeXonoticHeaderLabel(_("Country")));
 +
 +      me.TR(me);
 +              me.TDempty(me, 0.5);
 +              me.TD(me, 4.5, 2, e = makeXonoticLanguageList()); // todo: cl_country: create proper country list
 +
 +
 +      // ================
 +      //  GENDER SECTION
 +      // ================
 +      me.gotoRC(me, 13.5, 3.1); me.setFirstColumn(me, me.currentColumn); // BOTTOM SECTION, TOP POS
 +      //me.gotoRC(me, 19.5, 3.1); me.setFirstColumn(me, me.currentColumn); // BOTTOM SECTION, BOTTOM POS
 +      //me.gotoRC(me, 6.5, 3.1); me.setFirstColumn(me, me.currentColumn); // TOP SECTION, BOTTOM POS
 +      #if 0
 +              me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Gender:")));
-                       me.TD(me, 1, GENDERWIDTH_ITEM, e = makeXonoticRadioButton(3, "cl_gender", "2", _("Female")));
-                       me.TD(me, 1, GENDERWIDTH_ITEM, e = makeXonoticRadioButton(3, "cl_gender", "1", _("Male")));
-                       me.TD(me, 1, GENDERWIDTH_ITEM, e = makeXonoticRadioButton(3, "cl_gender", "0", _("Undisclosed")));
++              me.TD(me, 1, 2, e = makeXonoticTextSlider("_cl_gender"));
 +                      e.addValue(e, ZCTX(_("GENDER^Undisclosed")), "0");
 +                      e.addValue(e, ZCTX(_("GENDER^Female")), "1");
 +                      e.addValue(e, ZCTX(_("GENDER^Male")), "2");
 +                      e.configureXonoticTextSliderValues(e);
 +      #else
 +                      me.TD(me, 1, 3, e = makeXonoticHeaderLabel(_("Gender")));
 +              me.TR(me);
 +                      #define GENDERWIDTH_OFFSET 0.25
 +                      #define GENDERWIDTH_LENGTH 2.5
 +                      #define GENDERWIDTH_ITEM (GENDERWIDTH_LENGTH / 3)
 +                      me.TDempty(me, GENDERWIDTH_OFFSET);
++                      me.TD(me, 1, GENDERWIDTH_ITEM, e = makeXonoticRadioButton(3, "_cl_gender", "2", _("Female")));
++                      me.TD(me, 1, GENDERWIDTH_ITEM, e = makeXonoticRadioButton(3, "_cl_gender", "1", _("Male")));
++                      me.TD(me, 1, GENDERWIDTH_ITEM, e = makeXonoticRadioButton(3, "_cl_gender", "0", _("Undisclosed")));
 +      #endif
 +
 +      me.gotoRC(me, me.rows - 1, 0);
 +              me.TD(me, 1, me.columns, makeXonoticCommandButton(_("Apply immediately"), '0 0 0', "color -1 -1;name \"$_cl_name\";sendcvar cl_weaponpriority;sendcvar cl_autoswitch;sendcvar cl_forceplayermodels;sendcvar cl_forceplayermodelsfromxonotic;playermodel $_cl_playermodel;playerskin $_cl_playerskin", COMMANDBUTTON_APPLY));
 +}
 +#endif
index 81624fd,0000000..cccaa26
mode 100644,000000..100644
--- /dev/null
@@@ -1,166 -1,0 +1,170 @@@
-               me.TD(me, 1, 3, makeXonoticCheckBoxEx(2, 0, "menu_sounds", _("Menu sounds")));
 +#ifdef INTERFACE
 +CLASS(XonoticAudioSettingsTab) EXTENDS(XonoticTab)
 +      METHOD(XonoticAudioSettingsTab, fill, void(entity))
 +      ATTRIB(XonoticAudioSettingsTab, title, string, _("Audio"))
 +      ATTRIB(XonoticAudioSettingsTab, intendedWidth, float, 0.9)
 +      ATTRIB(XonoticAudioSettingsTab, rows, float, 15.5)
 +      ATTRIB(XonoticAudioSettingsTab, columns, float, 6.2) // added extra .2 for center space
++      ATTRIB(XonoticAudioSettingsTab, hiddenMenuSoundsSlider, entity, NULL)
 +ENDCLASS(XonoticAudioSettingsTab)
 +entity makeXonoticAudioSettingsTab();
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +entity makeXonoticAudioSettingsTab()
 +{
 +      entity me;
 +      me = spawnXonoticAudioSettingsTab();
 +      me.configureDialog(me);
 +      return me;
 +}
 +
 +void XonoticAudioSettingsTab_fill(entity me)
 +{
 +      entity e, s;
 +
 +      me.TR(me);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "mastervolume");
 +              me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Master:")));
 +              me.TD(me, 1, 2, s);
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "bgmvolume");
 +              makeMulti(s, "snd_channel8volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Music:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_staticvolume");
 +              makeMulti(s, "snd_channel9volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, ZCTX(_("VOL^Ambient:"))));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel0volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Info:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel3volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Items:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel6volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Pain:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel7volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Player:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel4volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Shots:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel2volume");
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Voice:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              s = makeXonoticDecibelsSlider(-40, 0, 0.4, "snd_channel1volume");
 +              makeMulti(s, "snd_channel5volume"); // @!#%'n Tuba
 +              me.TD(me, 1, 0.8, e = makeXonoticTextLabel(0, _("Weapons:")));
 +              me.TD(me, 1, 2, s);
 +              setDependentStringNotEqual(e, "mastervolume", "0");
 +              setDependentStringNotEqual(s, "mastervolume", "0");
 +      me.TR(me);
 +      me.TR(me);
 +              me.TD(me, 1, 3, makeXonoticCheckBox(0, "menu_snd_attenuation_method", _("New style sound attenuation")));
 +      me.TR(me);
 +              me.TD(me, 1, 3, makeXonoticCheckBox(0, "snd_mutewhenidle", _("Mute sounds when not active")));
 +
 +      me.gotoRC(me, 0, 3.2); me.setFirstColumn(me, me.currentColumn);
 +              me.TD(me, 1, 1, makeXonoticTextLabel(0, _("Frequency:")));
 +              me.TD(me, 1, 2, e = makeXonoticTextSlider("snd_speed"));
 +                      e.addValue(e, _("8 kHz"), "8000");
 +                      e.addValue(e, _("11.025 kHz"), "11025");
 +                      e.addValue(e, _("16 kHz"), "16000");
 +                      e.addValue(e, _("22.05 kHz"), "22050");
 +                      e.addValue(e, _("24 kHz"), "24000");
 +                      e.addValue(e, _("32 kHz"), "32000");
 +                      e.addValue(e, _("44.1 kHz"), "44100");
 +                      e.addValue(e, _("48 kHz"), "48000");
 +                      e.configureXonoticTextSliderValues(e);
 +      me.TR(me);
 +              me.TD(me, 1, 1, makeXonoticTextLabel(0, _("Channels:")));
 +              me.TD(me, 1, 2, e = makeXonoticTextSlider("snd_channels"));
 +                      e.addValue(e, _("Mono"), "1");
 +                      e.addValue(e, _("Stereo"), "2");
 +                      e.addValue(e, _("2.1"), "3");
 +                      e.addValue(e, _("4"), "4");
 +                      e.addValue(e, _("5"), "5");
 +                      e.addValue(e, _("5.1"), "6");
 +                      e.addValue(e, _("6.1"), "7");
 +                      e.addValue(e, _("7.1"), "8");
 +                      e.configureXonoticTextSliderValues(e);
 +      me.TR(me);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBox(0, "snd_swapstereo", _("Swap stereo output channels")));
 +              setDependent(e, "snd_channels", 1.5, 0.5);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBox(0, "snd_spatialization_control", _("Headphone friendly mode")));
 +              setDependent(e, "snd_channels", 1.5, 0.5);
 +      me.TR(me);
 +      me.TR(me);
 +              me.TD(me, 1, 3, makeXonoticCheckBox(0, "cl_hitsound", _("Hit indication sound")));
 +              e.sendCvars = true;
 +      me.TR(me);
 +              me.TD(me, 1, 3, makeXonoticCheckBox(0, "con_chatsound", _("Chat message sound")));
 +      me.TR(me);
++              me.hiddenMenuSoundsSlider = makeXonoticSlider(1, 1, 1, "menu_sounds");
++              me.TD(me, 1, 1.2, makeXonoticSliderCheckBox(0, 1, me.hiddenMenuSoundsSlider, _("Menu sounds")));
++              me.TD(me, 1, 1.8, e = makeXonoticSliderCheckBox(2, 0, me.hiddenMenuSoundsSlider, _("Focus sounds")));
++              setDependent(e, "menu_sounds", 1, 2);
 +      me.TR(me);
 +      me.TR(me);
 +              me.TD(me, 1, 1, makeXonoticTextLabel(0, _("Time announcer:")));
 +              me.TD(me, 1, 2, e = makeXonoticTextSlider("cl_announcer_maptime"));
 +                      e.addValue(e, ZCTX(_("WRN^Disabled")), "0");
 +                      e.addValue(e, _("1 minute"), "1");
 +                      e.addValue(e, _("5 minutes"), "2");
 +                      e.addValue(e, ZCTX(_("WRN^Both")), "3");
 +                      e.configureXonoticTextSliderValues(e);
 +      me.TR(me);
 +              me.TD(me, 1, 1, makeXonoticTextLabel(0, _("Automatic taunts:")));
 +              me.TD(me, 1, 2, e = makeXonoticTextSlider("cl_autotaunt"));
 +                      e.addValue(e, _("Never"), "0");
 +                      e.addValue(e, _("Sometimes"), "0.35");
 +                      e.addValue(e, _("Often"), "0.65");
 +                      e.addValue(e, _("Always"), "1");
 +                      e.configureXonoticTextSliderValues(e);
 +                      e.sendCvars = true;
 +      me.TR(me);
 +      me.TR(me);
 +              if(cvar("developer"))
 +                      me.TD(me, 1, 3, makeXonoticCheckBox(0, "showsound", _("Debug info about sounds")));
 +
 +      me.gotoRC(me, me.rows - 1, 0);
 +              me.TD(me, 1, me.columns, makeXonoticCommandButton(_("Apply immediately"), '0 0 0', "snd_restart; snd_attenuation_method_${menu_snd_attenuation_method}", COMMANDBUTTON_APPLY));
 +}
 +#endif
index 7e34151,0000000..0aa38e8
mode 100644,000000..100644
--- /dev/null
@@@ -1,163 -1,0 +1,168 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticGameCrosshairSettingsTab) EXTENDS(XonoticTab)
 +      //METHOD(XonoticGameCrosshairSettingsTab, toString, string(entity))
 +      METHOD(XonoticGameCrosshairSettingsTab, fill, void(entity))
 +      METHOD(XonoticGameCrosshairSettingsTab, showNotify, void(entity))
 +      ATTRIB(XonoticGameCrosshairSettingsTab, title, string, _("Crosshair"))
 +      ATTRIB(XonoticGameCrosshairSettingsTab, intendedWidth, float, 0.9)
 +      ATTRIB(XonoticGameCrosshairSettingsTab, rows, float, 13)
 +      ATTRIB(XonoticGameCrosshairSettingsTab, columns, float, 6.2)
 +ENDCLASS(XonoticGameCrosshairSettingsTab)
 +entity makeXonoticGameCrosshairSettingsTab();
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void XonoticGameCrosshairSettingsTab_showNotify(entity me)
 +{
 +      loadAllCvars(me);
 +}
 +entity makeXonoticGameCrosshairSettingsTab()
 +{
 +      entity me;
 +      me = spawnXonoticGameCrosshairSettingsTab();
 +      me.configureDialog(me);
 +      return me;
 +}
 +
 +void XonoticGameCrosshairSettingsTab_fill(entity me)
 +{
 +      entity e;
 +      float i;
 +
 +      // crosshair_enabled: 0 = no crosshair options, 1 = no crosshair selection, but everything else enabled, 2 = all crosshair options enabled
 +      // FIXME: In the future, perhaps make one global crosshair_type cvar which has 0 for disabled, 1 for custom, 2 for per weapon, etc?
 +      me.TR(me); //me.gotoRC(me, 0, 3.2); me.setFirstColumn(me, me.currentColumn);
 +              me.TD(me, 1, 1, e = makeXonoticRadioButton(3, "crosshair_enabled", "0", _("No crosshair")));
 +      //me.TR(me);
 +              me.TD(me, 1, 1, e = makeXonoticRadioButton(3, "crosshair_per_weapon", string_null, _("Per weapon")));
 +              makeMulti(e, "crosshair_enabled");
 +      //me.TR(me);
 +              me.TD(me, 1, 1, e = makeXonoticRadioButton(3, "crosshair_enabled", "2", _("Custom")));
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              for(i = 1; i <= 14; ++i) {
 +                      me.TDNoMargin(me, 1, 2 / 14, e = makeXonoticCrosshairButton(4, i), '1 1 0');
 +                              setDependentAND(e, "crosshair_per_weapon", 0, 0, "crosshair_enabled", 1, 2);
 +              }
 +              // show a larger preview of the selected crosshair
 +              me.TDempty(me, 0.1);
 +              me.TDNoMargin(me, 3, 0.8, e = makeXonoticCrosshairButton(7, -1), '1 1 0'); // crosshair -1 makes this a preview
 +                      setDependentAND(e, "crosshair_per_weapon", 0, 0, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              for(i = 15; i <= 28; ++i) {
 +                      me.TDNoMargin(me, 1, 2 / 14, e = makeXonoticCrosshairButton(4, i), '1 1 0');
 +                              setDependentAND(e, "crosshair_per_weapon", 0, 0, "crosshair_enabled", 1, 2);
 +              }
 +      me.TR(me);
++              me.TDempty(me, 0.1);
++              for(i = 29; i <= 42; ++i) {
++                      me.TDNoMargin(me, 1, 2 / 14, e = makeXonoticCrosshairButton(4, i), '1 1 0');
++                              setDependentAND(e, "crosshair_per_weapon", 0, 0, "crosshair_enabled", 1, 2);
++              }
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Crosshair size:")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 1.9, e = makeXonoticSlider(0.1, 1.0, 0.01, "crosshair_size"));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Crosshair alpha:")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 1.9, e = makeXonoticSlider(0, 1, 0.1, "crosshair_alpha"));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Crosshair color:")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 0.9, e = makeXonoticRadioButton(5, "crosshair_color_special", "1", _("Per weapon")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 1, e = makeXonoticRadioButton(5, "crosshair_color_special", "2", _("By health")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              me.TD(me, 1, 0.8, e = makeXonoticRadioButton(5, "crosshair_color_special", "0", _("Custom")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +              me.TD(me, 2, 2, e = makeXonoticColorpickerString("crosshair_color", "crosshair_color"));
 +                      setDependentAND(e, "crosshair_color_special", 0, 0, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +      me.TR(me);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 2.9, e = makeXonoticCheckBox(0, "crosshair_ring", _("Use rings to indicate weapon status")));
 +                      makeMulti(e, "crosshair_ring_reload");
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      //me.TR(me);
 +      //      me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Ring size:")));
 +      //              setDependentAND(e, "crosshair_ring", 1, 1, "crosshair_enabled", 1, 2);
 +      //      me.TD(me, 1, 2, e = makeXonoticSlider(2, 4, 0.1, "crosshair_ring_size"));
 +      //              setDependentAND(e, "crosshair_ring", 1, 1, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.3);
 +              me.TD(me, 1, 0.9, e = makeXonoticTextLabel(0, _("Ring alpha:")));
 +                      setDependentAND(e, "crosshair_ring", 1, 1, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 1.8, e = makeXonoticSlider(0.1, 1, 0.1, "crosshair_ring_alpha"));
 +                      setDependentAND(e, "crosshair_ring", 1, 1, "crosshair_enabled", 1, 2);
 +
 +      me.gotoRC(me, 0, 3.2); me.setFirstColumn(me, me.currentColumn);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBox(0, "crosshair_dot", _("Enable center crosshair dot")));
 +              setDependent(e, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 0.9, e = makeXonoticTextLabel(0, _("Dot size:")));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 2, e = makeXonoticSlider(0.2, 2, 0.1, "crosshair_dot_size"));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 0.9, e = makeXonoticTextLabel(0, _("Dot alpha:")));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 2, e = makeXonoticSlider(0.1, 1, 0.1, "crosshair_dot_alpha"));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.1);
 +              me.TD(me, 1, 0.9, e = makeXonoticTextLabel(0, _("Dot color:")));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +              me.TD(me, 1, 2, e = makeXonoticRadioButton(1, "crosshair_dot_color_custom", "0", _("Use normal crosshair color")));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TDempty(me, 0.2);
 +              me.TD(me, 1, 0.8, e = makeXonoticRadioButton(1, "crosshair_dot_color_custom", "1", _("Custom")));
 +                      setDependentAND(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2);
 +              me.TD(me, 2, 2, e = makeXonoticColorpickerString("crosshair_dot_color", "crosshair_dot_color"));
 +                      setDependentAND3(e, "crosshair_dot", 1, 1, "crosshair_enabled", 1, 2, "crosshair_dot_color_custom", 1, 1);
 +      me.TR(me);
 +      me.TR(me);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBox(0, "crosshair_effect_scalefade", _("Smooth effects of crosshairs")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBox(0, "crosshair_hittest_blur", _("Blur crosshair if the shot is obstructed")));
 +                      setDependentAND(e, "crosshair_hittest", 1, 100, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBoxEx(1.25, 0, "crosshair_hittest_scale", _("Enlarge crosshair if targeting an enemy")));
 +                      setDependentAND(e, "crosshair_hittest", 1, 100, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBoxEx(0.5, 0, "crosshair_hitindication", _("Animate crosshair when hitting an enemy")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      me.TR(me);
 +              me.TD(me, 1, 3, e = makeXonoticCheckBoxEx(0.25, 0, "crosshair_pickup", _("Animate crosshair when picking up an item")));
 +                      setDependent(e, "crosshair_enabled", 1, 2);
 +      /*me.TR(me);
 +              me.TD(me, 1, 1, e = makeXonoticTextLabel(0, _("Hit testing:")));
 +              me.TD(me, 1, 2, e = makeXonoticTextSlider("crosshair_hittest"));
 +                      e.addValue(e, ZCTX(_("HTTST^Disabled")), "0");
 +                      e.addValue(e, ZCTX(_("HTTST^TrueAim")), "1");
 +                      e.addValue(e, ZCTX(_("HTTST^Enemies")), "1.25");
 +                      e.configureXonoticTextSliderValues(e);
 +                      setDependent(e, "crosshair_enabled", 1, 2);*/
 +
 +      /*me.TR(me);
 +
 +      me.gotoRC(me, me.rows - 1, 0);
 +              me.TD(me, 1, me.columns, e = makeXonoticButton(_("OK"), '0 0 0'));
 +                      e.onClick = Dialog_Close;
 +                      e.onClickEntity = me;*/
 +}
 +#endif
index 8e584b8,0000000..0d1c05a
mode 100644,000000..100644
--- /dev/null
@@@ -1,25 -1,0 +1,30 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticWinnerDialog) EXTENDS(XonoticDialog)
 +      METHOD(XonoticWinnerDialog, fill, void(entity))
++      METHOD(XonoticWinnerDialog, focusEnter, void(entity))
 +      ATTRIB(XonoticWinnerDialog, title, string, _("Winner"))
 +      ATTRIB(XonoticWinnerDialog, color, vector, SKINCOLOR_DIALOG_SINGLEPLAYER)
 +      ATTRIB(XonoticWinnerDialog, intendedWidth, float, 0.32)
 +      ATTRIB(XonoticWinnerDialog, rows, float, 12)
 +      ATTRIB(XonoticWinnerDialog, columns, float, 3)
 +ENDCLASS(XonoticWinnerDialog)
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void XonoticWinnerDialog_fill(entity me)
 +{
 +      entity e;
 +
 +      me.TR(me);
 +              me.TD(me, me.rows - 2, me.columns, e = makeXonoticImage("/gfx/winner", -1));
 +
 +      me.gotoRC(me, me.rows - 1, 0);
 +              me.TD(me, 1, me.columns, e = makeXonoticButton(_("OK"), '0 0 0'));
 +                      e.onClick = Dialog_Close;
 +                      e.onClickEntity = me;
 +}
++void XonoticWinnerDialog_focusEnter(entity me)
++{
++      m_play_click_sound(MENU_SOUND_WINNER);
++}
 +#endif
index 45f493b,0000000..feb1d89
mode 100644,000000..100644
--- /dev/null
@@@ -1,125 -1,0 +1,129 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticGametypeList) EXTENDS(XonoticListBox)
 +      METHOD(XonoticGametypeList, configureXonoticGametypeList, void(entity))
 +      ATTRIB(XonoticGametypeList, rowsPerItem, float, 2)
 +      METHOD(XonoticGametypeList, drawListBoxItem, void(entity, float, vector, float))
 +      METHOD(XonoticGametypeList, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticGametypeList, setSelected, void(entity, float))
 +      METHOD(XonoticGametypeList, loadCvars, void(entity))
 +      METHOD(XonoticGametypeList, saveCvars, void(entity))
 +      METHOD(XonoticGametypeList, keyDown, float(entity, float, float, float))
++      METHOD(XonoticGametypeList, clickListBoxItem, void(entity, float, vector))
 +
 +      ATTRIB(XonoticGametypeList, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticGametypeList, realUpperMargin, float, 0)
 +      ATTRIB(XonoticGametypeList, columnIconOrigin, float, 0)
 +      ATTRIB(XonoticGametypeList, columnIconSize, float, 0)
 +      ATTRIB(XonoticGametypeList, columnNameOrigin, float, 0)
 +      ATTRIB(XonoticGametypeList, columnNameSize, float, 0)
 +ENDCLASS(XonoticGametypeList)
 +entity makeXonoticGametypeList();
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +
 +entity makeXonoticGametypeList(void)
 +{
 +      entity me;
 +      me = spawnXonoticGametypeList();
 +      me.configureXonoticGametypeList(me);
 +      return me;
 +}
 +void XonoticGametypeList_configureXonoticGametypeList(entity me)
 +{
 +      float i;
 +      me.configureXonoticListBox(me);
 +      me.nItems = GameType_GetCount();
 +
 +      // we want the pics mipmapped
 +      for(i = 0; i < GameType_GetCount(); ++i)
 +              draw_PreloadPictureWithFlags(GameType_GetIcon(i), PRECACHE_PIC_MIPMAP);
 +
 +      me.loadCvars(me);
 +}
 +void XonoticGametypeList_setSelected(entity me, float i)
 +{
 +      SUPER(XonoticGametypeList).setSelected(me, i);
 +      me.saveCvars(me);
 +}
 +void XonoticGametypeList_loadCvars(entity me)
 +{
 +      float t;
 +      t = MapInfo_CurrentGametype();
 +      float i;
 +      for(i = 0; i < GameType_GetCount(); ++i)
 +              if(t == GameType_GetID(i))
 +                      break;
 +      if(i >= GameType_GetCount())
 +      {
 +              for(i = 0; i < GameType_GetCount(); ++i)
 +                      if(t == MAPINFO_TYPE_DEATHMATCH)
 +                              break;
 +              if(i >= GameType_GetCount())
 +                      i = 0;
 +      }
 +      me.setSelected(me, i);
 +      // do we need this: me.parent.gameTypeChangeNotify(me.parent); // to make sure
 +}
 +void XonoticGametypeList_saveCvars(entity me)
 +{
 +      float t;
 +      t = GameType_GetID(me.selectedItem);
 +      if(t == MapInfo_CurrentGametype())
 +              return;
 +      MapInfo_SwitchGameType(t);
 +      me.parent.gameTypeChangeNotify(me.parent);
 +}
 +void XonoticGametypeList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      string s1, s2;
 +
 +      if(isSelected)
 +              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 +
 +      draw_Picture(me.columnIconOrigin * eX, GameType_GetIcon(i), me.columnIconSize * eX + eY, '1 1 1', SKINALPHA_LISTBOX_SELECTED);
 +      s1 = GameType_GetName(i);
 +
 +      if(_MapInfo_GetTeamPlayBool(GameType_GetID(i)))
 +              s2 = _("teamplay");
 +      else
 +              s2 = _("free for all");
 +
 +      vector save_fontscale = draw_fontscale;
 +      float f = draw_CondensedFontFactor(strcat(s1, " ", s2), false, me.realFontSize, 1);
 +      draw_fontscale.x *= f;
 +      vector fs = me.realFontSize;
 +      fs.x *= f;
 +      draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s1, fs, '1 1 1', SKINALPHA_TEXT, 0);
 +      draw_Text(me.realUpperMargin * eY + (me.columnNameOrigin + 1.0 * (me.columnNameSize - draw_TextWidth(s2, 0, fs))) * eX, s2, fs, SKINCOLOR_TEXT, SKINALPHA_TEXT, 0);
 +      draw_fontscale = save_fontscale;
 +}
 +void XonoticGametypeList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.itemAbsSize = '0 0 0';
 +      SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +
 +      me.realFontSize_y = me.fontSize / (me.itemAbsSize_y = (absSize.y * me.itemHeight));
 +      me.realFontSize_x = me.fontSize / (me.itemAbsSize_x = (absSize.x * (1 - me.controlWidth)));
 +      me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
 +      me.columnIconOrigin = 0;
 +      me.columnIconSize = me.itemAbsSize.y / me.itemAbsSize.x;
 +      me.columnNameOrigin = me.columnIconOrigin + me.columnIconSize + (0.5 * me.realFontSize.x);
 +      me.columnNameSize = 1 - me.columnIconSize - (1.5 * me.realFontSize.x);
 +}
 +float XonoticGametypeList_keyDown(entity me, float scan, float ascii, float shift)
 +{
 +      if(scan == K_ENTER || scan == K_KP_ENTER)
 +      {
++              m_play_click_sound(MENU_SOUND_EXECUTE);
 +              me.parent.gameTypeSelectNotify(me.parent);
 +              return 1;
 +      }
 +
 +      return SUPER(XonoticGametypeList).keyDown(me, scan, ascii, shift);
 +}
++void XonoticGametypeList_clickListBoxItem(entity me, float i, vector where)
++{
++      m_play_click_sound(MENU_SOUND_SELECT);
++}
 +#endif
index 7304202,0000000..a083207
mode 100644,000000..100644
--- /dev/null
@@@ -1,363 -1,0 +1,365 @@@
-       localcmd("exec binds-default.cfg\n");
 +#ifdef INTERFACE
 +CLASS(XonoticKeyBinder) EXTENDS(XonoticListBox)
 +      METHOD(XonoticKeyBinder, configureXonoticKeyBinder, void(entity))
 +      ATTRIB(XonoticKeyBinder, rowsPerItem, float, 1)
 +      METHOD(XonoticKeyBinder, drawListBoxItem, void(entity, float, vector, float))
 +      METHOD(XonoticKeyBinder, doubleClickListBoxItem, void(entity, float, vector))
 +      METHOD(XonoticKeyBinder, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticKeyBinder, setSelected, void(entity, float))
 +      METHOD(XonoticKeyBinder, keyDown, float(entity, float, float, float))
 +      METHOD(XonoticKeyBinder, keyGrabbed, void(entity, float, float))
 +
 +      ATTRIB(XonoticKeyBinder, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticKeyBinder, realUpperMargin, float, 0)
 +      ATTRIB(XonoticKeyBinder, columnFunctionOrigin, float, 0)
 +      ATTRIB(XonoticKeyBinder, columnFunctionSize, float, 0)
 +      ATTRIB(XonoticKeyBinder, columnKeysOrigin, float, 0)
 +      ATTRIB(XonoticKeyBinder, columnKeysSize, float, 0)
 +
 +      ATTRIB(XonoticKeyBinder, previouslySelected, float, -1)
 +      ATTRIB(XonoticKeyBinder, inMouseHandler, float, 0)
 +      ATTRIB(XonoticKeyBinder, userbindEditButton, entity, NULL)
 +      ATTRIB(XonoticKeyBinder, keyGrabButton, entity, NULL)
 +      ATTRIB(XonoticKeyBinder, clearButton, entity, NULL)
 +      ATTRIB(XonoticKeyBinder, userbindEditDialog, entity, NULL)
 +      METHOD(XonoticKeyBinder, editUserbind, void(entity, string, string, string))
 +ENDCLASS(XonoticKeyBinder)
 +entity makeXonoticKeyBinder();
 +void KeyBinder_Bind_Change(entity btn, entity me);
 +void KeyBinder_Bind_Clear(entity btn, entity me);
 +void KeyBinder_Bind_Edit(entity btn, entity me);
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +
 +const string KEY_NOT_BOUND_CMD = "// not bound";
 +
 +const float MAX_KEYS_PER_FUNCTION = 2;
 +const float MAX_KEYBINDS = 256;
 +string Xonotic_KeyBinds_Functions[MAX_KEYBINDS];
 +string Xonotic_KeyBinds_Descriptions[MAX_KEYBINDS];
 +float Xonotic_KeyBinds_Count = -1;
 +
 +void Xonotic_KeyBinds_Read()
 +{
 +      float fh;
 +      string s;
 +
 +      Xonotic_KeyBinds_Count = 0;
 +      fh = fopen(language_filename("keybinds.txt"), FILE_READ);
 +      if(fh < 0)
 +              return;
 +      while((s = fgets(fh)))
 +      {
 +              if(tokenize_console(s) != 2)
 +                      continue;
 +              Xonotic_KeyBinds_Functions[Xonotic_KeyBinds_Count] = strzone(argv(0));
 +              Xonotic_KeyBinds_Descriptions[Xonotic_KeyBinds_Count] = strzone(argv(1));
 +              ++Xonotic_KeyBinds_Count;
 +              if(Xonotic_KeyBinds_Count >= MAX_KEYBINDS)
 +                      break;
 +      }
 +      fclose(fh);
 +}
 +
 +entity makeXonoticKeyBinder()
 +{
 +      entity me;
 +      me = spawnXonoticKeyBinder();
 +      me.configureXonoticKeyBinder(me);
 +      return me;
 +}
 +void replace_bind(string from, string to)
 +{
 +      float n, j, k;
 +      n = tokenize(findkeysforcommand(from, 0)); // uses '...' strings
 +      for(j = 0; j < n; ++j)
 +      {
 +              k = stof(argv(j));
 +              if(k != -1)
 +                      localcmd("\nbind \"", keynumtostring(k), "\" \"", to, "\"\n");
 +      }
 +      if(n)
 +              cvar_set("_hud_showbinds_reload", "1");
 +}
 +void XonoticKeyBinder_configureXonoticKeyBinder(entity me)
 +{
 +      me.configureXonoticListBox(me);
 +      if(Xonotic_KeyBinds_Count < 0)
 +              Xonotic_KeyBinds_Read();
 +      me.nItems = Xonotic_KeyBinds_Count;
 +      me.setSelected(me, 0);
 +
 +      // TEMP: Xonotic 0.1 to later
 +      replace_bind("impulse 1", "weapon_group_1");
 +      replace_bind("impulse 2", "weapon_group_2");
 +      replace_bind("impulse 3", "weapon_group_3");
 +      replace_bind("impulse 4", "weapon_group_4");
 +      replace_bind("impulse 5", "weapon_group_5");
 +      replace_bind("impulse 6", "weapon_group_6");
 +      replace_bind("impulse 7", "weapon_group_7");
 +      replace_bind("impulse 8", "weapon_group_8");
 +      replace_bind("impulse 9", "weapon_group_9");
 +      replace_bind("impulse 14", "weapon_group_0");
 +}
 +void XonoticKeyBinder_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      SUPER(XonoticKeyBinder).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +
 +      me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
 +      me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
 +      me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
 +
 +      me.columnFunctionOrigin = 0;
 +      me.columnKeysSize = me.realFontSize.x * 12;
 +      me.columnFunctionSize = 1 - me.columnKeysSize - 2 * me.realFontSize.x;
 +      me.columnKeysOrigin = me.columnFunctionOrigin + me.columnFunctionSize + me.realFontSize.x;
 +
 +      if(me.userbindEditButton)
 +              me.userbindEditButton.disabled = (substring(Xonotic_KeyBinds_Descriptions[me.selectedItem], 0, 1) != "$");
 +}
 +void KeyBinder_Bind_Change(entity btn, entity me)
 +{
 +      string func;
 +
 +      func = Xonotic_KeyBinds_Functions[me.selectedItem];
 +      if(func == "")
 +              return;
 +
 +      me.keyGrabButton.forcePressed = 1;
 +      me.clearButton.disabled = 1;
 +      keyGrabber = me;
 +}
 +void XonoticKeyBinder_keyGrabbed(entity me, float key, float ascii)
 +{
 +      float n, j, k, nvalid;
 +      string func;
 +
 +      me.keyGrabButton.forcePressed = 0;
 +      me.clearButton.disabled = 0;
 +
 +      if(key == K_ESCAPE)
 +              return;
 +
 +      // forbid these keys from being bound in the menu
 +      if(key == K_CAPSLOCK || key == K_NUMLOCK)
 +      {
 +              KeyBinder_Bind_Change(me, me);
 +              return;
 +      }
 +
 +      func = Xonotic_KeyBinds_Functions[me.selectedItem];
 +      if(func == "")
 +              return;
 +
 +      n = tokenize(findkeysforcommand(func, 0)); // uses '...' strings
 +      nvalid = 0;
 +      for(j = 0; j < n; ++j)
 +      {
 +              k = stof(argv(j));
 +              if(k != -1)
 +                      ++nvalid;
 +      }
 +      if(nvalid >= MAX_KEYS_PER_FUNCTION)
 +      {
 +              for(j = 0; j < n; ++j)
 +              {
 +                      k = stof(argv(j));
 +                      if(k != -1)
 +                              //localcmd("\nunbind \"", keynumtostring(k), "\"\n");
 +                              localcmd("\nbind \"", keynumtostring(k), "\" \"", KEY_NOT_BOUND_CMD, "\"\n");
 +              }
 +      }
++      m_play_click_sound(MENU_SOUND_SELECT);
 +      localcmd("\nbind \"", keynumtostring(key), "\" \"", func, "\"\n");
 +      localcmd("-zoom\n"); // to make sure we aren't in togglezoom'd state
 +      cvar_set("_hud_showbinds_reload", "1");
 +}
 +void XonoticKeyBinder_editUserbind(entity me, string theName, string theCommandPress, string theCommandRelease)
 +{
 +      string func, descr;
 +
 +      if(!me.userbindEditDialog)
 +              return;
 +
 +      func = Xonotic_KeyBinds_Functions[me.selectedItem];
 +      if(func == "")
 +              return;
 +
 +      descr = Xonotic_KeyBinds_Descriptions[me.selectedItem];
 +      if(substring(descr, 0, 1) != "$")
 +              return;
 +      descr = substring(descr, 1, strlen(descr) - 1);
 +
 +      // Hooray! It IS a user bind!
 +      cvar_set(strcat(descr, "_description"), theName);
 +      cvar_set(strcat(descr, "_press"), theCommandPress);
 +      cvar_set(strcat(descr, "_release"), theCommandRelease);
 +}
 +void KeyBinder_Bind_Edit(entity btn, entity me)
 +{
 +      string func, descr;
 +
 +      if(!me.userbindEditDialog)
 +              return;
 +
 +      func = Xonotic_KeyBinds_Functions[me.selectedItem];
 +      if(func == "")
 +              return;
 +
 +      descr = Xonotic_KeyBinds_Descriptions[me.selectedItem];
 +      if(substring(descr, 0, 1) != "$")
 +              return;
 +      descr = substring(descr, 1, strlen(descr) - 1);
 +
 +      // Hooray! It IS a user bind!
 +      me.userbindEditDialog.loadUserBind(me.userbindEditDialog, cvar_string(strcat(descr, "_description")), cvar_string(strcat(descr, "_press")), cvar_string(strcat(descr, "_release")));
 +
 +      DialogOpenButton_Click(btn, me.userbindEditDialog);
 +}
 +void KeyBinder_Bind_Clear(entity btn, entity me)
 +{
 +      float n, j, k;
 +      string func;
 +
 +      func = Xonotic_KeyBinds_Functions[me.selectedItem];
 +      if(func == "")
 +              return;
 +
 +      n = tokenize(findkeysforcommand(func, 0)); // uses '...' strings
 +      for(j = 0; j < n; ++j)
 +      {
 +              k = stof(argv(j));
 +              if(k != -1)
 +                      //localcmd("\nunbind \"", keynumtostring(k), "\"\n");
 +                      localcmd("\nbind \"", keynumtostring(k), "\" \"", KEY_NOT_BOUND_CMD, "\"\n");
 +      }
++      m_play_click_sound(MENU_SOUND_CLEAR);
 +      localcmd("-zoom\n"); // to make sure we aren't in togglezoom'd state
 +      cvar_set("_hud_showbinds_reload", "1");
 +}
 +void KeyBinder_Bind_Reset_All(entity btn, entity me)
 +{
 +      localcmd("unbindall\n");
++      localcmd("exec binds-xonotic.cfg\n");
 +      localcmd("-zoom\n"); // to make sure we aren't in togglezoom'd state
 +      cvar_set("_hud_showbinds_reload", "1");
 +}
 +void XonoticKeyBinder_doubleClickListBoxItem(entity me, float i, vector where)
 +{
 +      KeyBinder_Bind_Change(NULL, me);
 +}
 +void XonoticKeyBinder_setSelected(entity me, float i)
 +{
 +      // handling of "unselectable" items
 +      i = floor(0.5 + bound(0, i, me.nItems - 1));
 +      if(me.pressed == 0 || me.pressed == 1) // keyboard or scrolling - skip unselectable items
 +      {
 +              if(i > me.previouslySelected)
 +              {
 +                      while((i < me.nItems - 1) && (Xonotic_KeyBinds_Functions[i] == ""))
 +                              ++i;
 +              }
 +              while((i > 0) && (Xonotic_KeyBinds_Functions[i] == ""))
 +                      --i;
 +              while((i < me.nItems - 1) && (Xonotic_KeyBinds_Functions[i] == ""))
 +                      ++i;
 +      }
 +      if(me.pressed == 3) // released the mouse - fall back to last valid item
 +      {
 +              if(Xonotic_KeyBinds_Functions[i] == "")
 +                      i = me.previouslySelected;
 +      }
 +      if(Xonotic_KeyBinds_Functions[i] != "")
 +              me.previouslySelected = i;
 +      if(me.userbindEditButton)
 +              me.userbindEditButton.disabled = (substring(Xonotic_KeyBinds_Descriptions[i], 0, 1) != "$");
 +      SUPER(XonoticKeyBinder).setSelected(me, i);
 +}
 +float XonoticKeyBinder_keyDown(entity me, float key, float ascii, float shift)
 +{
 +      float r;
 +      r = 1;
 +      switch(key)
 +      {
 +              case K_ENTER:
 +              case K_KP_ENTER:
 +              case K_SPACE:
 +                      KeyBinder_Bind_Change(me, me);
 +                      break;
 +              case K_DEL:
 +              case K_KP_DEL:
 +              case K_BACKSPACE:
 +                      KeyBinder_Bind_Clear(me, me);
 +                      break;
 +              default:
 +                      r = SUPER(XonoticKeyBinder).keyDown(me, key, ascii, shift);
 +                      break;
 +      }
 +      return r;
 +}
 +void XonoticKeyBinder_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      string s;
 +      float j, k, n;
 +      vector theColor;
 +      float theAlpha;
 +      string func, descr;
 +      float extraMargin;
 +
 +      descr = Xonotic_KeyBinds_Descriptions[i];
 +      func = Xonotic_KeyBinds_Functions[i];
 +
 +      if(func == "")
 +      {
 +              theAlpha = 1;
 +              theColor = SKINCOLOR_KEYGRABBER_TITLES;
 +              theAlpha = SKINALPHA_KEYGRABBER_TITLES;
 +              extraMargin = 0;
 +      }
 +      else
 +      {
 +              if(isSelected)
 +              {
 +                      if(keyGrabber == me)
 +                              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_WAITING, SKINALPHA_LISTBOX_WAITING);
 +                      else
 +                              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 +              }
 +              theAlpha = SKINALPHA_KEYGRABBER_KEYS;
 +              theColor = SKINCOLOR_KEYGRABBER_KEYS;
 +              extraMargin = me.realFontSize.x * 0.5;
 +      }
 +
 +      if(substring(descr, 0, 1) == "$")
 +      {
 +              s = substring(descr, 1, strlen(descr) - 1);
 +              descr = cvar_string(strcat(s, "_description"));
 +              if(descr == "")
 +                      descr = s;
 +              if(cvar_string(strcat(s, "_press")) == "")
 +                      if(cvar_string(strcat(s, "_release")) == "")
 +                              theAlpha *= SKINALPHA_DISABLED;
 +      }
 +
 +      s = draw_TextShortenToWidth(descr, me.columnFunctionSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin * eY + extraMargin * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +      if(func != "")
 +      {
 +              n = tokenize(findkeysforcommand(func, 0)); // uses '...' strings
 +              s = "";
 +              for(j = 0; j < n; ++j)
 +              {
 +                      k = stof(argv(j));
 +                      if(k != -1)
 +                      {
 +                              if(s != "")
 +                                      s = strcat(s, ", ");
 +                              s = strcat(s, keynumtostring(k));
 +                      }
 +              }
 +              s = draw_TextShortenToWidth(s, me.columnKeysSize, 0, me.realFontSize);
 +              draw_CenterText(me.realUpperMargin * eY + (me.columnKeysOrigin + 0.5 * me.columnKeysSize) * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +      }
 +}
 +#endif
index 2d43a47,0000000..8aa0d2c
mode 100644,000000..100644
--- /dev/null
@@@ -1,215 -1,0 +1,218 @@@
-       if(scan == K_ENTER || scan == K_KP_ENTER) {
 +#ifdef INTERFACE
 +CLASS(XonoticLanguageList) EXTENDS(XonoticListBox)
 +      METHOD(XonoticLanguageList, configureXonoticLanguageList, void(entity))
 +      ATTRIB(XonoticLanguageList, rowsPerItem, float, 1)
 +      METHOD(XonoticLanguageList, drawListBoxItem, void(entity, float, vector, float))
 +      METHOD(XonoticLanguageList, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticLanguageList, setSelected, void(entity, float))
 +      METHOD(XonoticLanguageList, loadCvars, void(entity))
 +      METHOD(XonoticLanguageList, saveCvars, void(entity))
 +
 +      ATTRIB(XonoticLanguageList, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticLanguageList, realUpperMargin, float, 0)
 +      ATTRIB(XonoticLanguageList, columnNameOrigin, float, 0)
 +      ATTRIB(XonoticLanguageList, columnNameSize, float, 0)
 +      ATTRIB(XonoticLanguageList, columnPercentageOrigin, float, 0)
 +      ATTRIB(XonoticLanguageList, columnPercentageSize, float, 0)
 +
 +      METHOD(XonoticLanguageList, doubleClickListBoxItem, void(entity, float, vector))
 +      METHOD(XonoticLanguageList, keyDown, float(entity, float, float, float)) // enter handling
 +
 +      METHOD(XonoticLanguageList, destroy, void(entity))
 +
 +      ATTRIB(XonoticLanguageList, languagelist, float, -1)
 +      METHOD(XonoticLanguageList, getLanguages, void(entity))
 +      METHOD(XonoticLanguageList, setLanguage, void(entity))
 +      METHOD(XonoticLanguageList, languageParameter, string(entity, float, float))
 +
 +      ATTRIB(XonoticLanguageList, name, string, "languageselector") // change this to make it noninteractive (for first run dialog)
 +ENDCLASS(XonoticLanguageList)
 +
 +entity makeXonoticLanguageList();
 +void SetLanguage_Click(entity btn, entity me);
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +
 +const float LANGPARM_ID = 0;
 +const float LANGPARM_NAME = 1;
 +const float LANGPARM_NAME_LOCALIZED = 2;
 +const float LANGPARM_PERCENTAGE = 3;
 +const float LANGPARM_COUNT = 4;
 +
 +entity makeXonoticLanguageList()
 +{
 +      entity me;
 +      me = spawnXonoticLanguageList();
 +      me.configureXonoticLanguageList(me);
 +      return me;
 +}
 +
 +void XonoticLanguageList_configureXonoticLanguageList(entity me)
 +{
 +      me.configureXonoticListBox(me);
 +      me.getLanguages(me);
 +      me.loadCvars(me);
 +}
 +
 +void XonoticLanguageList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      string s, p;
 +      if(isSelected)
 +              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 +
 +      s = me.languageParameter(me, i, LANGPARM_NAME_LOCALIZED);
 +
 +      vector save_fontscale = draw_fontscale;
 +      float f = draw_CondensedFontFactor(s, false, me.realFontSize, 1);
 +      draw_fontscale.x *= f;
 +      vector fs = me.realFontSize;
 +      fs.x *= f;
 +      draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, fs, SKINCOLOR_TEXT, SKINALPHA_TEXT, 0);
 +      draw_fontscale = save_fontscale;
 +
 +      p = me.languageParameter(me, i, LANGPARM_PERCENTAGE);
 +      if(p != "")
 +      {
 +              vector save_fontscale = draw_fontscale;
 +              float f = draw_CondensedFontFactor(p, false, me.realFontSize, 1);
 +              draw_fontscale.x *= f;
 +              vector fs = me.realFontSize;
 +              fs.x *= f;
 +              draw_Text(me.realUpperMargin * eY + (me.columnPercentageOrigin + (me.columnPercentageSize - draw_TextWidth(p, 0, fs))) * eX, p, fs, SKINCOLOR_TEXT, SKINALPHA_TEXT, 0);
 +              draw_fontscale = save_fontscale;
 +      }
 +}
 +
 +void XonoticLanguageList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      SUPER(XonoticLanguageList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +      me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
 +      me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
 +      me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
 +      me.columnPercentageSize = me.realFontSize.x * 3;
 +      me.columnPercentageOrigin = 1 - me.columnPercentageSize;
 +      me.columnNameOrigin = 0;
 +      me.columnNameSize = me.columnPercentageOrigin;
 +}
 +
 +void XonoticLanguageList_setSelected(entity me, float i)
 +{
 +      SUPER(XonoticLanguageList).setSelected(me, i);
 +      me.saveCvars(me);
 +}
 +
 +void XonoticLanguageList_loadCvars(entity me)
 +{
 +      string s;
 +      float i, n;
 +      s = cvar_string("_menu_prvm_language");
 +      n = me.nItems;
 +
 +      // default to English
 +      for(i = 0; i < n; ++i)
 +      {
 +              if(me.languageParameter(me, i, LANGPARM_ID) == "en")
 +              {
 +                      me.selectedItem = i;
 +                      break;
 +              }
 +      }
 +
 +        // otherwise, find the language
 +      for(i = 0; i < n; ++i)
 +      {
 +              if(me.languageParameter(me, i, LANGPARM_ID) == s)
 +              {
 +                      me.selectedItem = i;
 +                      break;
 +              }
 +      }
 +
 +      // save it off (turning anything unknown into "en")
 +      me.saveCvars(me);
 +}
 +
 +void XonoticLanguageList_saveCvars(entity me)
 +{
 +      cvar_set("_menu_prvm_language", me.languageParameter(me, me.selectedItem, LANGPARM_ID));
 +}
 +
 +void XonoticLanguageList_doubleClickListBoxItem(entity me, float i, vector where)
 +{
++      m_play_click_sound(MENU_SOUND_EXECUTE);
 +      me.setLanguage(me);
 +}
 +
 +float XonoticLanguageList_keyDown(entity me, float scan, float ascii, float shift)
 +{
++      if(scan == K_ENTER || scan == K_KP_ENTER)
++      {
++              m_play_click_sound(MENU_SOUND_EXECUTE);
 +              me.setLanguage(me);
 +              return 1;
 +      }
 +      else
 +              return SUPER(XonoticLanguageList).keyDown(me, scan, ascii, shift);
 +}
 +
 +void XonoticLanguageList_destroy(entity me)
 +{
 +      buf_del(me.languagelist);
 +}
 +
 +void XonoticLanguageList_getLanguages(entity me)
 +{
 +      float buf, i, n, fh;
 +      string s;
 +
 +      buf = buf_create();
 +
 +      fh = fopen("languages.txt", FILE_READ);
 +      i = 0;
 +      while((s = fgets(fh)))
 +      {
 +              n = tokenize_console(s);
 +              if(n < 3)
 +                      continue;
 +              bufstr_set(buf, i * LANGPARM_COUNT + LANGPARM_ID, argv(0));
 +              bufstr_set(buf, i * LANGPARM_COUNT + LANGPARM_NAME, argv(1));
 +              float k = strstrofs(argv(2), "(", 0);
 +              if(k > 0)
 +              if(substring(argv(2), strlen(argv(2)) - 1, 1) == ")")
 +              {
 +                      string percent = substring(argv(2), k + 1, -2);
 +                      if(percent != "100%")
 +                              bufstr_set(buf, i * LANGPARM_COUNT + LANGPARM_PERCENTAGE, percent);
 +              }
 +              bufstr_set(buf, i * LANGPARM_COUNT + LANGPARM_NAME_LOCALIZED, (k < 0) ? argv(2) : substring(argv(2), 0, k - 1));
 +              ++i;
 +      }
 +      fclose(fh);
 +
 +      me.languagelist = buf;
 +      me.nItems = i;
 +}
 +
 +void XonoticLanguageList_setLanguage(entity me)
 +{
 +      if(prvm_language != cvar_string("_menu_prvm_language"))
 +      {
 +              if(!(gamestatus & GAME_CONNECTED))
 +                      localcmd("\nprvm_language \"$_menu_prvm_language\"; menu_restart; menu_cmd languageselect\n");
 +              else
 +                      DialogOpenButton_Click(me, main.languageWarningDialog);
 +      }
 +}
 +
 +string XonoticLanguageList_languageParameter(entity me, float i, float key)
 +{
 +      return bufstr_get(me.languagelist, i * LANGPARM_COUNT + key);
 +}
 +
 +void SetLanguage_Click(entity btn, entity me)
 +{
 +      me.setLanguage(me);
 +}
 +
 +#endif
index 60720bd,0000000..d88ad0e
mode 100644,000000..100644
--- /dev/null
@@@ -1,353 -1,0 +1,365 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticMapList) EXTENDS(XonoticListBox)
 +      METHOD(XonoticMapList, configureXonoticMapList, void(entity))
 +      ATTRIB(XonoticMapList, rowsPerItem, float, 4)
 +      METHOD(XonoticMapList, draw, void(entity))
 +      METHOD(XonoticMapList, drawListBoxItem, void(entity, float, vector, float))
 +      METHOD(XonoticMapList, clickListBoxItem, void(entity, float, vector))
 +      METHOD(XonoticMapList, doubleClickListBoxItem, void(entity, float, vector))
 +      METHOD(XonoticMapList, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticMapList, refilter, void(entity))
 +      METHOD(XonoticMapList, refilterCallback, void(entity, entity))
 +      METHOD(XonoticMapList, keyDown, float(entity, float, float, float))
 +
 +      ATTRIB(XonoticMapList, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticMapList, columnPreviewOrigin, float, 0)
 +      ATTRIB(XonoticMapList, columnPreviewSize, float, 0)
 +      ATTRIB(XonoticMapList, columnNameOrigin, float, 0)
 +      ATTRIB(XonoticMapList, columnNameSize, float, 0)
 +      ATTRIB(XonoticMapList, checkMarkOrigin, vector, '0 0 0')
 +      ATTRIB(XonoticMapList, checkMarkSize, vector, '0 0 0')
 +      ATTRIB(XonoticMapList, realUpperMargin1, float, 0)
 +      ATTRIB(XonoticMapList, realUpperMargin2, float, 0)
 +
 +      ATTRIB(XonoticMapList, lastGametype, float, 0)
 +      ATTRIB(XonoticMapList, lastFeatures, float, 0)
 +
 +      ATTRIB(XonoticMapList, origin, vector, '0 0 0')
 +      ATTRIB(XonoticMapList, itemAbsSize, vector, '0 0 0')
 +
 +      ATTRIB(XonoticMapList, g_maplistCache, string, string_null)
 +      METHOD(XonoticMapList, g_maplistCacheToggle, void(entity, float))
 +      METHOD(XonoticMapList, g_maplistCacheQuery, float(entity, float))
 +
 +      ATTRIB(XonoticMapList, startButton, entity, NULL)
 +
 +      METHOD(XonoticMapList, loadCvars, void(entity))
 +
 +      ATTRIB(XonoticMapList, typeToSearchString, string, string_null)
 +      ATTRIB(XonoticMapList, typeToSearchTime, float, 0)
 +
 +      METHOD(XonoticMapList, destroy, void(entity))
 +
 +      ATTRIB(XonoticListBox, alphaBG, float, 0)
 +ENDCLASS(XonoticMapList)
 +entity makeXonoticMapList();
 +void MapList_All(entity btn, entity me);
 +void MapList_None(entity btn, entity me);
 +void MapList_LoadMap(entity btn, entity me);
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +void XonoticMapList_destroy(entity me)
 +{
 +      MapInfo_Shutdown();
 +}
 +
 +entity makeXonoticMapList()
 +{
 +      entity me;
 +      me = spawnXonoticMapList();
 +      me.configureXonoticMapList(me);
 +      return me;
 +}
 +
 +void XonoticMapList_configureXonoticMapList(entity me)
 +{
 +      me.configureXonoticListBox(me);
 +      me.refilter(me);
 +}
 +
 +void XonoticMapList_loadCvars(entity me)
 +{
 +      me.refilter(me);
 +}
 +
 +float XonoticMapList_g_maplistCacheQuery(entity me, float i)
 +{
 +      return stof(substring(me.g_maplistCache, i, 1));
 +}
 +void XonoticMapList_g_maplistCacheToggle(entity me, float i)
 +{
 +      string a, b, c, s, bspname;
 +      float n;
 +      s = me.g_maplistCache;
 +      if (!s)
 +              return;
 +      b = substring(s, i, 1);
 +      if(b == "0")
 +              b = "1";
 +      else if(b == "1")
 +              b = "0";
 +      else
 +              return; // nothing happens
 +      a = substring(s, 0, i);
 +      c = substring(s, i+1, strlen(s) - (i+1));
 +      strunzone(s);
 +      me.g_maplistCache = strzone(strcat(a, b, c));
 +      // TODO also update the actual cvar
 +      if (!((bspname = MapInfo_BSPName_ByID(i))))
 +              return;
 +      if(b == "1")
 +              cvar_set("g_maplist", strcat(bspname, " ", cvar_string("g_maplist")));
 +      else
 +      {
 +              s = "";
 +              n = tokenize_console(cvar_string("g_maplist"));
 +              for(i = 0; i < n; ++i)
 +                      if(argv(i) != bspname)
 +                              s = strcat(s, " ", argv(i));
 +              cvar_set("g_maplist", substring(s, 1, strlen(s) - 1));
 +      }
 +}
 +
 +void XonoticMapList_draw(entity me)
 +{
 +      if(me.startButton)
 +              me.startButton.disabled = ((me.selectedItem < 0) || (me.selectedItem >= me.nItems));
 +      SUPER(XonoticMapList).draw(me);
 +}
 +
 +void XonoticMapList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.itemAbsSize = '0 0 0';
 +      SUPER(XonoticMapList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +
 +      me.realFontSize_y = me.fontSize / (me.itemAbsSize_y = (absSize.y * me.itemHeight));
 +      me.realFontSize_x = me.fontSize / (me.itemAbsSize_x = (absSize.x * (1 - me.controlWidth)));
 +      me.realUpperMargin1 = 0.5 * (1 - 2.5 * me.realFontSize.y);
 +      me.realUpperMargin2 = me.realUpperMargin1 + 1.5 * me.realFontSize.y;
 +
 +      me.columnPreviewOrigin = 0;
 +      me.columnPreviewSize = me.itemAbsSize.y / me.itemAbsSize.x * 4 / 3;
 +      me.columnNameOrigin = me.columnPreviewOrigin + me.columnPreviewSize + me.realFontSize.x;
 +      me.columnNameSize = 1 - me.columnPreviewSize - 2 * me.realFontSize.x;
 +
 +      me.checkMarkSize = (eX * (me.itemAbsSize.y / me.itemAbsSize.x) + eY) * 0.5;
 +      me.checkMarkOrigin = eY + eX * (me.columnPreviewOrigin + me.columnPreviewSize) - me.checkMarkSize;
 +}
 +
 +void XonoticMapList_clickListBoxItem(entity me, float i, vector where)
 +{
 +      if(where.x <= me.columnPreviewOrigin + me.columnPreviewSize)
 +              if(where.x >= 0)
++              {
++                      m_play_click_sound(MENU_SOUND_SELECT);
 +                      me.g_maplistCacheToggle(me, i);
++              }
 +}
 +
 +void XonoticMapList_doubleClickListBoxItem(entity me, float i, vector where)
 +{
 +      if(where.x >= me.columnNameOrigin)
 +              if(where.x <= 1)
 +              {
 +                      // pop up map info screen
++                      m_play_click_sound(MENU_SOUND_OPEN);
 +                      main.mapInfoDialog.loadMapInfo(main.mapInfoDialog, i, me);
 +                      DialogOpenButton_Click_withCoords(NULL, main.mapInfoDialog, me.origin + eX * (me.columnNameOrigin * me.size.x) + eY * ((me.itemHeight * i - me.scrollPos) * me.size.y), eY * me.itemAbsSize.y + eX * (me.itemAbsSize.x * me.columnNameSize));
 +              }
 +}
 +
 +void XonoticMapList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      // layout: Ping, Map name, Map name, NP, TP, MP
 +      string s;
 +      float theAlpha;
 +      float included;
 +
 +      if(!MapInfo_Get_ByID(i))
 +              return;
 +
 +      included = me.g_maplistCacheQuery(me, i);
 +      if(included || isSelected)
 +              theAlpha = SKINALPHA_MAPLIST_INCLUDEDFG;
 +      else
 +              theAlpha = SKINALPHA_MAPLIST_NOTINCLUDEDFG;
 +
 +      if(isSelected)
 +              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 +      else if(included)
 +              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_MAPLIST_INCLUDEDBG, SKINALPHA_MAPLIST_INCLUDEDBG);
 +
 +      if(draw_PictureSize(strcat("/maps/", MapInfo_Map_bspname)) == '0 0 0')
 +              draw_Picture(me.columnPreviewOrigin * eX, "nopreview_map", me.columnPreviewSize * eX + eY, '1 1 1', theAlpha);
 +      else
 +              draw_Picture(me.columnPreviewOrigin * eX, strcat("/maps/", MapInfo_Map_bspname), me.columnPreviewSize * eX + eY, '1 1 1', theAlpha);
 +
 +      if(included)
 +              draw_Picture(me.checkMarkOrigin, "checkmark", me.checkMarkSize, '1 1 1', 1);
 +      s = draw_TextShortenToWidth(strdecolorize(MapInfo_Map_titlestring), me.columnNameSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin1 * eY + (me.columnNameOrigin + 0.00 * (me.columnNameSize - draw_TextWidth(s, 0, me.realFontSize))) * eX, s, me.realFontSize, SKINCOLOR_MAPLIST_TITLE, theAlpha, 0);
 +      s = draw_TextShortenToWidth(strdecolorize(MapInfo_Map_author), me.columnNameSize, 0,  me.realFontSize);
 +      draw_Text(me.realUpperMargin2 * eY + (me.columnNameOrigin + 1.00 * (me.columnNameSize - draw_TextWidth(s, 0, me.realFontSize))) * eX, s, me.realFontSize, SKINCOLOR_MAPLIST_AUTHOR, theAlpha, 0);
 +
 +      MapInfo_ClearTemps();
 +}
 +
 +void XonoticMapList_refilter(entity me)
 +{
 +      float i, j, n;
 +      string s;
 +      float gt, f;
 +      gt = MapInfo_CurrentGametype();
 +      f = MapInfo_CurrentFeatures();
 +      MapInfo_FilterGametype(gt, f, MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags(), 0);
 +      me.nItems = MapInfo_count;
 +      for(i = 0; i < MapInfo_count; ++i)
 +              draw_PreloadPicture(strcat("/maps/", MapInfo_BSPName_ByID(i)));
 +      if(me.g_maplistCache)
 +              strunzone(me.g_maplistCache);
 +      s = "0";
 +      for(i = 1; i < MapInfo_count; i *= 2)
 +              s = strcat(s, s);
 +      n = tokenize_console(cvar_string("g_maplist"));
 +      for(i = 0; i < n; ++i)
 +      {
 +              j = MapInfo_FindName(argv(i));
 +              if(j >= 0)
 +                      s = strcat(
 +                              substring(s, 0, j),
 +                              "1",
 +                              substring(s, j+1, MapInfo_count - (j+1))
 +                      );
 +      }
 +      me.g_maplistCache = strzone(s);
 +      if(gt != me.lastGametype || f != me.lastFeatures)
 +      {
 +              me.lastGametype = gt;
 +              me.lastFeatures = f;
 +              me.setSelected(me, 0);
 +      }
 +}
 +
 +void XonoticMapList_refilterCallback(entity me, entity cb)
 +{
 +      me.refilter(me);
 +}
 +
 +void MapList_All(entity btn, entity me)
 +{
 +      float i;
 +      string s;
 +      MapInfo_FilterGametype(MAPINFO_TYPE_ALL, 0, 0, MapInfo_ForbiddenFlags(), 0); // all
 +      s = "";
 +      for(i = 0; i < MapInfo_count; ++i)
 +              s = strcat(s, " ", MapInfo_BSPName_ByID(i));
 +      cvar_set("g_maplist", substring(s, 1, strlen(s) - 1));
 +      me.refilter(me);
 +}
 +
 +void MapList_None(entity btn, entity me)
 +{
 +      cvar_set("g_maplist", "");
 +      me.refilter(me);
 +}
 +
 +void MapList_LoadMap(entity btn, entity me)
 +{
 +      string m;
 +      float i;
 +
 +      i = me.selectedItem;
 +
 +      if(btn.parent.instanceOfXonoticMapInfoDialog)
 +      {
 +              i = btn.parent.currentMapIndex;
 +              Dialog_Close(btn, btn.parent);
 +      }
 +
 +      if(i >= me.nItems || i < 0)
 +              return;
 +
 +      m = MapInfo_BSPName_ByID(i);
 +      if (!m)
 +      {
 +              print(_("Huh? Can't play this (m is NULL). Refiltering so this won't happen again.\n"));
 +              me.refilter(me);
 +              return;
 +      }
 +      if(MapInfo_CheckMap(m))
 +      {
 +              localcmd("\nmenu_loadmap_prepare\n");
 +              if(cvar("menu_use_default_hostname"))
 +                      localcmd("hostname \"", sprintf(_("%s's Xonotic Server"), strdecolorize(cvar_string("_cl_name"))), "\"\n");
 +              MapInfo_LoadMap(m, 1);
 +      }
 +      else
 +      {
 +              print(_("Huh? Can't play this (invalid game type). Refiltering so this won't happen again.\n"));
 +              me.refilter(me);
 +              return;
 +      }
 +}
 +
 +float XonoticMapList_keyDown(entity me, float scan, float ascii, float shift)
 +{
 +      string ch, save;
 +      if(me.nItems <= 0)
 +              return SUPER(XonoticMapList).keyDown(me, scan, ascii, shift);
 +      if(scan == K_MOUSE2 || scan == K_SPACE || scan == K_ENTER || scan == K_KP_ENTER)
 +      {
 +              // pop up map info screen
++              m_play_click_sound(MENU_SOUND_OPEN);
 +              main.mapInfoDialog.loadMapInfo(main.mapInfoDialog, me.selectedItem, me);
 +              DialogOpenButton_Click_withCoords(NULL, main.mapInfoDialog, me.origin + eX * (me.columnNameOrigin * me.size.x) + eY * ((me.itemHeight * me.selectedItem - me.scrollPos) * me.size.y), eY * me.itemAbsSize.y + eX * (me.itemAbsSize.x * me.columnNameSize));
 +      }
 +      else if(scan == K_MOUSE3 || scan == K_INS || scan == K_KP_INS)
 +      {
++              m_play_click_sound(MENU_SOUND_SELECT);
 +              me.g_maplistCacheToggle(me, me.selectedItem);
 +      }
 +      else if(ascii == 43) // +
 +      {
 +              if (!me.g_maplistCacheQuery(me, me.selectedItem))
++              {
++                      m_play_click_sound(MENU_SOUND_SELECT);
 +                      me.g_maplistCacheToggle(me, me.selectedItem);
++              }
 +      }
 +      else if(ascii == 45) // -
 +      {
 +              if(me.g_maplistCacheQuery(me, me.selectedItem))
++              {
++                      m_play_click_sound(MENU_SOUND_SELECT);
 +                      me.g_maplistCacheToggle(me, me.selectedItem);
++              }
 +      }
 +      else if(scan == K_BACKSPACE)
 +      {
 +              if(time < me.typeToSearchTime)
 +              {
 +                      save = substring(me.typeToSearchString, 0, strlen(me.typeToSearchString) - 1);
 +                      if(me.typeToSearchString)
 +                              strunzone(me.typeToSearchString);
 +                      me.typeToSearchString = strzone(save);
 +                      me.typeToSearchTime = time + 0.5;
 +                      if(strlen(me.typeToSearchString))
 +                      {
 +                              MapInfo_FindName(me.typeToSearchString);
 +                              if(MapInfo_FindName_firstResult >= 0)
 +                                      me.setSelected(me, MapInfo_FindName_firstResult);
 +                      }
 +              }
 +      }
 +      else if(ascii >= 32 && ascii != 127)
 +      {
 +              ch = chr(ascii);
 +              if(time > me.typeToSearchTime)
 +                      save = ch;
 +              else
 +                      save = strcat(me.typeToSearchString, ch);
 +              if(me.typeToSearchString)
 +                      strunzone(me.typeToSearchString);
 +              me.typeToSearchString = strzone(save);
 +              me.typeToSearchTime = time + 0.5;
 +              MapInfo_FindName(me.typeToSearchString);
 +              if(MapInfo_FindName_firstResult >= 0)
 +                      me.setSelected(me, MapInfo_FindName_firstResult);
 +      }
 +      else
 +              return SUPER(XonoticMapList).keyDown(me, scan, ascii, shift);
 +      return 1;
 +}
 +
 +#endif
index 8daf730,0000000..d4dc5af
mode 100644,000000..100644
--- /dev/null
@@@ -1,138 -1,0 +1,139 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticPlayerList) EXTENDS(XonoticListBox)
 +      ATTRIB(XonoticPlayerList, rowsPerItem, float, 1)
 +      METHOD(XonoticPlayerList, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticPlayerList, drawListBoxItem, void(entity, float, vector, float))
++      ATTRIB(XonoticPlayerList, allowFocusSound, float, 0)
 +      ATTRIB(XonoticPlayerList, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticPlayerList, columnNameOrigin, float, 0)
 +      ATTRIB(XonoticPlayerList, columnNameSize, float, 0)
 +      ATTRIB(XonoticPlayerList, columnScoreOrigin, float, 0)
 +      ATTRIB(XonoticPlayerList, columnScoreSize, float, 0)
 +      ATTRIB(XonoticPlayerList, realUpperMargin, float, 0)
 +      ATTRIB(XonoticPlayerList, origin, vector, '0 0 0')
 +      ATTRIB(XonoticPlayerList, itemAbsSize, vector, '0 0 0')
 +      METHOD(XonoticPlayerList, setPlayerList, void(entity, string))
 +      METHOD(XonoticPlayerList, getPlayerList, string(entity, float, float))
 +      ATTRIB(XonoticPlayerList, playerList, float, -1)
 +ENDCLASS(XonoticPlayerList)
 +entity makeXonoticPlayerList();
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +
 +const float PLAYERPARM_SCORE = 0;
 +const float PLAYERPARM_PING = 1;
 +const float PLAYERPARM_TEAM = 2;
 +const float PLAYERPARM_NAME = 3;
 +const float PLAYERPARM_COUNT = 4;
 +
 +entity makeXonoticPlayerList()
 +{
 +      entity me;
 +      me = spawnXonoticPlayerList();
 +      me.configureXonoticListBox(me);
 +      return me;
 +}
 +
 +void XonoticPlayerList_setPlayerList(entity me, string plist)
 +{
 +      int buf,i,n;
 +      string s;
 +
 +      buf = buf_create();
 +      me.nItems = tokenizebyseparator(plist, "\n");
 +      for(i = 0; i < me.nItems; ++i)
 +      {
 +              bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_NAME, argv(i)); // -666 100 "^4Nex ^2Player"
 +      }
 +
 +      for(i = 0; i < me.nItems; ++i)
 +      {
 +              s = bufstr_get(buf, i * PLAYERPARM_COUNT + PLAYERPARM_NAME);
 +              n = tokenize_console(s);
 +
 +              if(n == 4)
 +              {
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_SCORE, argv(0)); // -666
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_PING,  argv(1)); // 100
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_TEAM,  argv(2)); // 0 for spec, else 1, 2, 3, 4
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_NAME,  argv(3)); // ^4Nex ^2Player
 +              }
 +              else
 +              {
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_SCORE, argv(0)); // -666
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_PING,  argv(1)); // 100
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_TEAM,  "-1");
 +                      bufstr_set(buf, i * PLAYERPARM_COUNT + PLAYERPARM_NAME,  argv(2)); // ^4Nex ^2Player
 +              }
 +      }
 +      me.playerList = buf;
 +}
 +
 +string XonoticPlayerList_getPlayerList(entity me, float i, float key)
 +{
 +      return bufstr_get(me.playerList, i * PLAYERPARM_COUNT + key);
 +}
 +
 +void XonoticPlayerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.itemAbsSize = '0 0 0';
 +      SUPER(XonoticPlayerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +
 +      me.realFontSize_y = me.fontSize / (me.itemAbsSize_y = (absSize.y * me.itemHeight));
 +      me.realFontSize_x = me.fontSize / (me.itemAbsSize_x = (absSize.x * (1 - me.controlWidth)));
 +      me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
 +
 +      // this list does 1 char left and right margin
 +      me.columnScoreSize = 5 * me.realFontSize.x;
 +      me.columnNameSize = 1 - 3 * me.realFontSize.x - me.columnScoreSize;
 +
 +      me.columnNameOrigin = me.realFontSize.x;
 +      me.columnScoreOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
 +}
 +
 +void XonoticPlayerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      string s;
 +      string score;
 +      float t;
 +      vector rgb;
 +
 +      t = stof(me.getPlayerList(me, i, PLAYERPARM_TEAM));
 +      if(t == 1)
 +              rgb = colormapPaletteColor(4, 0);
 +      else if(t == 2)
 +              rgb = colormapPaletteColor(13, 0);
 +      else if(t == 3)
 +              rgb = colormapPaletteColor(12, 0);
 +      else if(t == 4)
 +              rgb = colormapPaletteColor(9, 0);
 +      else
 +              rgb = SKINCOLOR_TEXT;
 +
 +      s = me.getPlayerList(me, i, PLAYERPARM_NAME);
 +      score = me.getPlayerList(me, i, PLAYERPARM_SCORE);
 +
 +      if(substring(score, strlen(score) - 10, 10) == ":spectator")
 +      {
 +              score = _("spectator");
 +      }
 +      else
 +      {
 +              if((t = strstrofs(score, ":", 0)) >= 0)
 +                      score = substring(score, 0, t);
 +              if((t = strstrofs(score, ",", 0)) >= 0)
 +                      score = substring(score, 0, t);
 +
 +              if(stof(score) == -666)
 +                      score = _("spectator");
 +      }
 +
 +      s = draw_TextShortenToWidth(s, me.columnNameSize, 1, me.realFontSize);
 +      draw_Text(me.realUpperMargin2 * eY + (me.columnNameOrigin + 0.00 * (me.columnNameSize - draw_TextWidth(s, 1, me.realFontSize))) * eX, s, me.realFontSize, '1 1 1', 1, 1);
 +
 +      score = draw_TextShortenToWidth(score, me.columnScoreSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin2 * eY + (me.columnScoreOrigin + 1.00 * (me.columnScoreSize - draw_TextWidth(score, 1, me.realFontSize))) * eX, score, me.realFontSize, rgb, 1, 0);
 +}
 +
 +#endif
index db0fbfe,0000000..c6c53cc
mode 100644,000000..100644
--- /dev/null
@@@ -1,1314 -1,0 +1,1317 @@@
 +#ifdef INTERFACE
 +CLASS(XonoticServerList) EXTENDS(XonoticListBox)
 +      METHOD(XonoticServerList, configureXonoticServerList, void(entity))
 +      ATTRIB(XonoticServerList, rowsPerItem, float, 1)
 +      METHOD(XonoticServerList, draw, void(entity))
 +      METHOD(XonoticServerList, drawListBoxItem, void(entity, float, vector, float))
 +      METHOD(XonoticServerList, doubleClickListBoxItem, void(entity, float, vector))
 +      METHOD(XonoticServerList, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticServerList, keyDown, float(entity, float, float, float))
 +      METHOD(XonoticServerList, toggleFavorite, void(entity, string))
 +
 +      ATTRIB(XonoticServerList, iconsSizeFactor, float, 0.85)
 +
 +      ATTRIB(XonoticServerList, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticServerList, realUpperMargin, float, 0)
 +      ATTRIB(XonoticServerList, columnIconsOrigin, float, 0)
 +      ATTRIB(XonoticServerList, columnIconsSize, float, 0)
 +      ATTRIB(XonoticServerList, columnPingOrigin, float, 0)
 +      ATTRIB(XonoticServerList, columnPingSize, float, 0)
 +      ATTRIB(XonoticServerList, columnNameOrigin, float, 0)
 +      ATTRIB(XonoticServerList, columnNameSize, float, 0)
 +      ATTRIB(XonoticServerList, columnMapOrigin, float, 0)
 +      ATTRIB(XonoticServerList, columnMapSize, float, 0)
 +      ATTRIB(XonoticServerList, columnTypeOrigin, float, 0)
 +      ATTRIB(XonoticServerList, columnTypeSize, float, 0)
 +      ATTRIB(XonoticServerList, columnPlayersOrigin, float, 0)
 +      ATTRIB(XonoticServerList, columnPlayersSize, float, 0)
 +
 +      ATTRIB(XonoticServerList, selectedServer, string, string_null) // to restore selected server when needed
 +      METHOD(XonoticServerList, setSelected, void(entity, float))
 +      METHOD(XonoticServerList, setSortOrder, void(entity, float, float))
 +      ATTRIB(XonoticServerList, filterShowEmpty, float, 1)
 +      ATTRIB(XonoticServerList, filterShowFull, float, 1)
 +      ATTRIB(XonoticServerList, filterString, string, string_null)
 +      ATTRIB(XonoticServerList, controlledTextbox, entity, NULL)
 +      ATTRIB(XonoticServerList, ipAddressBox, entity, NULL)
 +      ATTRIB(XonoticServerList, favoriteButton, entity, NULL)
 +      ATTRIB(XonoticServerList, nextRefreshTime, float, 0)
 +      METHOD(XonoticServerList, refreshServerList, void(entity, float)) // refresh mode: REFRESHSERVERLIST_*
 +      ATTRIB(XonoticServerList, needsRefresh, float, 1)
 +      METHOD(XonoticServerList, focusEnter, void(entity))
 +      METHOD(XonoticServerList, positionSortButton, void(entity, entity, float, float, string, void(entity, entity)))
 +      ATTRIB(XonoticServerList, sortButton1, entity, NULL)
 +      ATTRIB(XonoticServerList, sortButton2, entity, NULL)
 +      ATTRIB(XonoticServerList, sortButton3, entity, NULL)
 +      ATTRIB(XonoticServerList, sortButton4, entity, NULL)
 +      ATTRIB(XonoticServerList, sortButton5, entity, NULL)
 +      ATTRIB(XonoticServerList, connectButton, entity, NULL)
 +      ATTRIB(XonoticServerList, infoButton, entity, NULL)
 +      ATTRIB(XonoticServerList, currentSortOrder, float, 0)
 +      ATTRIB(XonoticServerList, currentSortField, float, -1)
 +
 +      ATTRIB(XonoticServerList, ipAddressBoxFocused, float, -1)
 +
 +      ATTRIB(XonoticServerList, seenIPv4, float, 0)
 +      ATTRIB(XonoticServerList, seenIPv6, float, 0)
 +      ATTRIB(XonoticServerList, categoriesHeight, float, 1.25)
 +
 +      METHOD(XonoticServerList, getTotalHeight, float(entity))
 +      METHOD(XonoticServerList, getItemAtPos, float(entity, float))
 +      METHOD(XonoticServerList, getItemStart, float(entity, float))
 +      METHOD(XonoticServerList, getItemHeight, float(entity, float))
 +ENDCLASS(XonoticServerList)
 +entity makeXonoticServerList();
 +
 +#ifndef IMPLEMENTATION
 +float autocvar_menu_slist_categories;
 +float autocvar_menu_slist_categories_onlyifmultiple;
 +float autocvar_menu_slist_purethreshold;
 +float autocvar_menu_slist_modimpurity;
 +float autocvar_menu_slist_recommendations;
 +float autocvar_menu_slist_recommendations_maxping;
 +float autocvar_menu_slist_recommendations_minfreeslots;
 +float autocvar_menu_slist_recommendations_minhumans;
 +float autocvar_menu_slist_recommendations_purethreshold;
 +
 +// server cache fields
 +#define SLIST_FIELDS \
 +      SLIST_FIELD(CNAME,       "cname") \
 +      SLIST_FIELD(PING,        "ping") \
 +      SLIST_FIELD(GAME,        "game") \
 +      SLIST_FIELD(MOD,         "mod") \
 +      SLIST_FIELD(MAP,         "map") \
 +      SLIST_FIELD(NAME,        "name") \
 +      SLIST_FIELD(MAXPLAYERS,  "maxplayers") \
 +      SLIST_FIELD(NUMPLAYERS,  "numplayers") \
 +      SLIST_FIELD(NUMHUMANS,   "numhumans") \
 +      SLIST_FIELD(NUMBOTS,     "numbots") \
 +      SLIST_FIELD(PROTOCOL,    "protocol") \
 +      SLIST_FIELD(FREESLOTS,   "freeslots") \
 +      SLIST_FIELD(PLAYERS,     "players") \
 +      SLIST_FIELD(QCSTATUS,    "qcstatus") \
 +      SLIST_FIELD(CATEGORY,    "category") \
 +      SLIST_FIELD(ISFAVORITE,  "isfavorite")
 +
 +#define SLIST_FIELD(suffix,name) float SLIST_FIELD_##suffix;
 +SLIST_FIELDS
 +#undef SLIST_FIELD
 +
 +const float REFRESHSERVERLIST_RESORT = 0;    // sort the server list again to update for changes to e.g. favorite status, categories
 +const float REFRESHSERVERLIST_REFILTER = 1;  // ..., also update filter and sort criteria
 +const float REFRESHSERVERLIST_ASK = 2;       // ..., also suggest querying servers now
 +const float REFRESHSERVERLIST_RESET = 3;     // ..., also clear the list first
 +
 +// function declarations
 +float IsServerInList(string list, string srv);
 +#define IsFavorite(srv) IsServerInList(cvar_string("net_slist_favorites"), srv)
 +#define IsPromoted(srv) IsServerInList(_Nex_ExtResponseSystem_PromotedServers, srv)
 +#define IsRecommended(srv) IsServerInList(_Nex_ExtResponseSystem_RecommendedServers, srv)
 +
 +entity RetrieveCategoryEnt(float catnum);
 +
 +float CheckCategoryOverride(float cat);
 +float CheckCategoryForEntry(float entry);
 +float m_gethostcachecategory(float entry) { return CheckCategoryOverride(CheckCategoryForEntry(entry)); }
 +
 +void RegisterSLCategories();
 +
 +void ServerList_Connect_Click(entity btn, entity me);
 +void ServerList_Categories_Click(entity box, entity me);
 +void ServerList_ShowEmpty_Click(entity box, entity me);
 +void ServerList_ShowFull_Click(entity box, entity me);
 +void ServerList_Filter_Change(entity box, entity me);
 +void ServerList_Favorite_Click(entity btn, entity me);
 +void ServerList_Info_Click(entity btn, entity me);
 +void ServerList_Update_favoriteButton(entity btn, entity me);
 +
 +// fields for category entities
 +const float MAX_CATEGORIES = 9;
 +const float CATEGORY_FIRST = 1;
 +entity categories[MAX_CATEGORIES];
 +float category_ent_count;
 +.string cat_name;
 +.string cat_string;
 +.string cat_enoverride_string;
 +.string cat_dioverride_string;
 +.float cat_enoverride;
 +.float cat_dioverride;
 +
 +// fields for drawing categories
 +float category_name[MAX_CATEGORIES];
 +float category_item[MAX_CATEGORIES];
 +float category_draw_count;
 +
 +#define SLIST_CATEGORIES \
 +      SLIST_CATEGORY(CAT_FAVORITED,    "",            "",             ZCTX(_("SLCAT^Favorites"))) \
 +      SLIST_CATEGORY(CAT_RECOMMENDED,  "",            "",             ZCTX(_("SLCAT^Recommended"))) \
 +      SLIST_CATEGORY(CAT_NORMAL,       "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Normal Servers"))) \
 +      SLIST_CATEGORY(CAT_SERVERS,      "CAT_NORMAL",  "CAT_SERVERS",  ZCTX(_("SLCAT^Servers"))) \
 +      SLIST_CATEGORY(CAT_XPM,          "CAT_NORMAL",  "CAT_SERVERS",  ZCTX(_("SLCAT^Competitive Mode"))) \
 +      SLIST_CATEGORY(CAT_MODIFIED,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Modified Servers"))) \
 +      SLIST_CATEGORY(CAT_OVERKILL,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Overkill Mode"))) \
 +      SLIST_CATEGORY(CAT_INSTAGIB,     "",            "CAT_SERVERS",  ZCTX(_("SLCAT^InstaGib Mode"))) \
 +      SLIST_CATEGORY(CAT_DEFRAG,       "",            "CAT_SERVERS",  ZCTX(_("SLCAT^Defrag Mode")))
 +
 +#define SLIST_CATEGORY_AUTOCVAR(name) autocvar_menu_slist_categories_##name##_override
 +#define SLIST_CATEGORY(name,enoverride,dioverride,str) \
 +      float name; \
 +      string SLIST_CATEGORY_AUTOCVAR(name) = enoverride;
 +SLIST_CATEGORIES
 +#undef SLIST_CATEGORY
 +
 +#endif
 +#endif
 +#ifdef IMPLEMENTATION
 +
 +void RegisterSLCategories()
 +{
 +      entity cat;
 +      #define SLIST_CATEGORY(name,enoverride,dioverride,str) \
 +              SET_FIELD_COUNT(name, CATEGORY_FIRST, category_ent_count) \
 +              CHECK_MAX_COUNT(name, MAX_CATEGORIES, category_ent_count, "SLIST_CATEGORY") \
 +              cat = spawn(); \
 +              categories[name - 1] = cat; \
 +              cat.classname = "slist_category"; \
 +              cat.cat_name = strzone(#name); \
 +              cat.cat_enoverride_string = strzone(SLIST_CATEGORY_AUTOCVAR(name)); \
 +              cat.cat_dioverride_string = strzone(dioverride); \
 +              cat.cat_string = strzone(str);
 +      SLIST_CATEGORIES
 +      #undef SLIST_CATEGORY
 +
 +      float i, x, catnum;
 +      string s;
 +
 +      #define PROCESS_OVERRIDE(override_string,override_field) \
 +              for(i = 0; i < category_ent_count; ++i) \
 +              { \
 +                      s = categories[i].override_string; \
 +                      if((s != "") && (s != categories[i].cat_name)) \
 +                      { \
 +                              catnum = 0; \
 +                              for(x = 0; x < category_ent_count; ++x) \
 +                              { if(categories[x].cat_name == s) { \
 +                                      catnum = (x+1); \
 +                                      break; \
 +                              } } \
 +                              if(catnum) \
 +                              { \
 +                                      strunzone(categories[i].override_string); \
 +                                      categories[i].override_field = catnum; \
 +                                      continue; \
 +                              } \
 +                              else \
 +                              { \
 +                                      printf( \
 +                                              "RegisterSLCategories(): Improper override '%s' for category '%s'!\n", \
 +                                              s, \
 +                                              categories[i].cat_name \
 +                                      ); \
 +                              } \
 +                      } \
 +                      strunzone(categories[i].override_string); \
 +                      categories[i].override_field = 0; \
 +              }
 +      PROCESS_OVERRIDE(cat_enoverride_string, cat_enoverride)
 +      PROCESS_OVERRIDE(cat_dioverride_string, cat_dioverride)
 +      #undef PROCESS_OVERRIDE
 +}
 +
 +// Supporting Functions
 +entity RetrieveCategoryEnt(float catnum)
 +{
 +      if((catnum > 0) && (catnum <= category_ent_count))
 +      {
 +              return categories[catnum - 1];
 +      }
 +      else
 +      {
 +              error(sprintf("RetrieveCategoryEnt(%d): Improper category number!\n", catnum));
 +              return world;
 +      }
 +}
 +
 +float IsServerInList(string list, string srv)
 +{
 +      string p;
 +      float i, n;
 +      if(srv == "")
 +              return false;
 +      srv = netaddress_resolve(srv, 26000);
 +      if(srv == "")
 +              return false;
 +      p = crypto_getidfp(srv);
 +      n = tokenize_console(list);
 +      for(i = 0; i < n; ++i)
 +      {
 +              if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
 +              {
 +                      if(p)
 +                              if(argv(i) == p)
 +                                      return true;
 +              }
 +              else
 +              {
 +                      if(srv == netaddress_resolve(argv(i), 26000))
 +                              return true;
 +              }
 +      }
 +      return false;
 +}
 +
 +float CheckCategoryOverride(float cat)
 +{
 +      entity catent = RetrieveCategoryEnt(cat);
 +      if(catent)
 +      {
 +              float override = (autocvar_menu_slist_categories ? catent.cat_enoverride : catent.cat_dioverride);
 +              if(override) { return override; }
 +              else { return cat; }
 +      }
 +      else
 +      {
 +              error(sprintf("CheckCategoryOverride(%d): Improper category number!\n", cat));
 +              return cat;
 +      }
 +}
 +
 +float CheckCategoryForEntry(float entry)
 +{
 +      string s, k, v, modtype = "";
 +      float j, m, impure = 0, freeslots = 0, sflags = 0;
 +      s = gethostcachestring(SLIST_FIELD_QCSTATUS, entry);
 +      m = tokenizebyseparator(s, ":");
 +
 +      for(j = 2; j < m; ++j)
 +      {
 +              if(argv(j) == "") { break; }
 +              k = substring(argv(j), 0, 1);
 +              v = substring(argv(j), 1, -1);
 +              switch(k)
 +              {
 +                      case "P": { impure = stof(v); break; }
 +                      case "S": { freeslots = stof(v); break; }
 +                      case "F": { sflags = stof(v); break; }
 +                      case "M": { modtype = strtolower(v); break; }
 +              }
 +      }
 +
 +      if(modtype != "xonotic") { impure += autocvar_menu_slist_modimpurity; }
 +
 +      // check if this server is favorited
 +      if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, entry)) { return CAT_FAVORITED; }
 +
 +      // now check if it's recommended
 +      if(autocvar_menu_slist_recommendations)
 +      {
 +              string cname = gethostcachestring(SLIST_FIELD_CNAME, entry);
 +
 +              if(IsPromoted(cname)) { return CAT_RECOMMENDED; }
 +              else
 +              {
 +                      float recommended = 0;
 +                      if(autocvar_menu_slist_recommendations & 1)
 +                      {
 +                              if(IsRecommended(cname)) { ++recommended; }
 +                              else { --recommended; }
 +                      }
 +                      if(autocvar_menu_slist_recommendations & 2)
 +                      {
 +                              if(
 +                                      ///// check for minimum free slots
 +                                      (freeslots >= autocvar_menu_slist_recommendations_minfreeslots)
 +
 +                                      && // check for purity requirement
 +                                      (
 +                                              (autocvar_menu_slist_recommendations_purethreshold < 0)
 +                                              ||
 +                                              (impure <= autocvar_menu_slist_recommendations_purethreshold)
 +                                      )
 +
 +                                      && // check for minimum amount of humans
 +                                      (
 +                                              gethostcachenumber(SLIST_FIELD_NUMHUMANS, entry)
 +                                              >=
 +                                              autocvar_menu_slist_recommendations_minhumans
 +                                      )
 +
 +                                      && // check for maximum latency
 +                                      (
 +                                              gethostcachenumber(SLIST_FIELD_PING, entry)
 +                                              <=
 +                                              autocvar_menu_slist_recommendations_maxping
 +                                      )
 +                              )
 +                                      { ++recommended; }
 +                              else
 +                                      { --recommended; }
 +                      }
 +                      if(recommended > 0) { return CAT_RECOMMENDED; }
 +              }
 +      }
 +
 +      // if not favorited or recommended, check modname
 +      if(modtype != "xonotic")
 +      {
 +              switch(modtype)
 +              {
 +                      // old servers which don't report their mod name are considered modified now
 +                      case "": { return CAT_MODIFIED; }
 +
 +                      case "xpm": { return CAT_XPM; }
 +                      case "minstagib":
 +                      case "instagib": { return CAT_INSTAGIB; }
 +                      case "overkill": { return CAT_OVERKILL; }
 +                      //case "nix": { return CAT_NIX; }
 +                      //case "newtoys": { return CAT_NEWTOYS; }
 +
 +                      // "cts" is allowed as compat, xdf is replacement
 +                      case "cts":
 +                      case "xdf": { return CAT_DEFRAG; }
 +
 +                      default: { dprintf("Found strange mod type: %s\n", modtype); return CAT_MODIFIED; }
 +              }
 +      }
 +
 +      // must be normal or impure server
 +      return ((impure > autocvar_menu_slist_purethreshold) ? CAT_MODIFIED : CAT_NORMAL);
 +}
 +
 +void XonoticServerList_toggleFavorite(entity me, string srv)
 +{
 +      string s, s0, s1, s2, srv_resolved, p;
 +      float i, n, f;
 +      srv_resolved = netaddress_resolve(srv, 26000);
 +      p = crypto_getidfp(srv_resolved);
 +      s = cvar_string("net_slist_favorites");
 +      n = tokenize_console(s);
 +      f = 0;
 +      for(i = 0; i < n; ++i)
 +      {
 +              if(substring(argv(i), 0, 1) != "[" && strlen(argv(i)) == 44 && strstrofs(argv(i), ".", 0) < 0)
 +              {
 +                      if(p)
 +                              if(argv(i) != p)
 +                                      continue;
 +              }
 +              else
 +              {
 +                      if(srv_resolved != netaddress_resolve(argv(i), 26000))
 +                              continue;
 +              }
 +              s0 = s1 = s2 = "";
 +              if(i > 0)
 +                      s0 = substring(s, 0, argv_end_index(i - 1));
 +              if(i < n-1)
 +                      s2 = substring(s, argv_start_index(i + 1), -1);
 +              if(s0 != "" && s2 != "")
 +                      s1 = " ";
 +              cvar_set("net_slist_favorites", strcat(s0, s1, s2));
 +              s = cvar_string("net_slist_favorites");
 +              n = tokenize_console(s);
 +              f = 1;
 +              --i;
 +      }
 +
 +      if(!f)
 +      {
 +              s1 = "";
 +              if(s != "")
 +                      s1 = " ";
 +              if(p)
 +                      cvar_set("net_slist_favorites", strcat(s, s1, p));
 +              else
 +                      cvar_set("net_slist_favorites", strcat(s, s1, srv));
 +      }
 +
 +      me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
 +}
 +
 +void ServerList_Update_favoriteButton(entity btn, entity me)
 +{
 +      me.favoriteButton.setText(me.favoriteButton,
 +              (IsFavorite(me.ipAddressBox.text) ?
 +                      _("Remove") : _("Favorite")
 +              )
 +      );
 +}
 +
 +entity makeXonoticServerList()
 +{
 +      entity me;
 +      me = spawnXonoticServerList();
 +      me.configureXonoticServerList(me);
 +      return me;
 +}
 +void XonoticServerList_configureXonoticServerList(entity me)
 +{
 +      me.configureXonoticListBox(me);
 +
 +      // update field ID's
 +      #define SLIST_FIELD(suffix,name) SLIST_FIELD_##suffix = gethostcacheindexforkey(name);
 +      SLIST_FIELDS
 +      #undef SLIST_FIELD
 +
 +      // clear list
 +      me.nItems = 0;
 +}
 +void XonoticServerList_setSelected(entity me, float i)
 +{
 +      float save;
 +      save = me.selectedItem;
 +      SUPER(XonoticServerList).setSelected(me, i);
 +      /*
 +      if(me.selectedItem == save)
 +              return;
 +      */
 +      if(me.nItems == 0)
 +              return;
 +      if(gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT) != me.nItems)
 +              return; // sorry, it would be wrong
 +
 +      if(me.selectedServer)
 +              strunzone(me.selectedServer);
 +      me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
 +
 +      me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
 +      me.ipAddressBox.cursorPos = strlen(me.selectedServer);
 +      me.ipAddressBoxFocused = -1;
 +}
 +void XonoticServerList_refreshServerList(entity me, float mode)
 +{
 +      //print("refresh of type ", ftos(mode), "\n");
 +
 +      if(mode >= REFRESHSERVERLIST_REFILTER)
 +      {
 +              float m, i, n;
 +              float listflags = 0;
 +              string s, typestr, modstr;
 +
 +              s = me.filterString;
 +
 +              m = strstrofs(s, ":", 0);
 +              if(m >= 0)
 +              {
 +                      typestr = substring(s, 0, m);
 +                      s = substring(s, m + 1, strlen(s) - m - 1);
 +                      while(substring(s, 0, 1) == " ")
 +                              s = substring(s, 1, strlen(s) - 1);
 +              }
 +              else
 +                      typestr = "";
 +
 +              modstr = cvar_string("menu_slist_modfilter");
 +
 +              m = SLIST_MASK_AND - 1;
 +              resethostcachemasks();
 +
 +              // ping: reject negative ping (no idea why this happens in the first place, engine bug)
 +              sethostcachemasknumber(++m, SLIST_FIELD_PING, 0, SLIST_TEST_GREATEREQUAL);
 +
 +              // show full button
 +              if(!me.filterShowFull)
 +              {
 +                      sethostcachemasknumber(++m, SLIST_FIELD_FREESLOTS, 1, SLIST_TEST_GREATEREQUAL); // legacy
 +                      sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, ":S0:", SLIST_TEST_NOTCONTAIN); // g_maxplayers support
 +              }
 +
 +              // show empty button
 +              if(!me.filterShowEmpty)
 +                      sethostcachemasknumber(++m, SLIST_FIELD_NUMHUMANS, 1, SLIST_TEST_GREATEREQUAL);
 +
 +              // gametype filtering
 +              if(typestr != "")
 +                      sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(typestr, ":"), SLIST_TEST_STARTSWITH);
 +
 +              // mod filtering
 +              if(modstr != "")
 +              {
 +                      if(substring(modstr, 0, 1) == "!")
 +                              sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(substring(modstr, 1, strlen(modstr) - 1)), SLIST_TEST_NOTEQUAL);
 +                      else
 +                              sethostcachemaskstring(++m, SLIST_FIELD_MOD, resolvemod(modstr), SLIST_TEST_EQUAL);
 +              }
 +
 +              // server banning
 +              n = tokenizebyseparator(_Nex_ExtResponseSystem_BannedServers, " ");
 +              for(i = 0; i < n; ++i)
 +                      if(argv(i) != "")
 +                              sethostcachemaskstring(++m, SLIST_FIELD_CNAME, argv(i), SLIST_TEST_NOTSTARTSWITH);
 +
 +              m = SLIST_MASK_OR - 1;
 +              if(s != "")
 +              {
 +                      sethostcachemaskstring(++m, SLIST_FIELD_NAME, s, SLIST_TEST_CONTAINS);
 +                      sethostcachemaskstring(++m, SLIST_FIELD_MAP, s, SLIST_TEST_CONTAINS);
 +                      sethostcachemaskstring(++m, SLIST_FIELD_PLAYERS, s, SLIST_TEST_CONTAINS);
 +                      sethostcachemaskstring(++m, SLIST_FIELD_QCSTATUS, strcat(s, ":"), SLIST_TEST_STARTSWITH);
 +              }
 +
 +              // sorting flags
 +              //listflags |= SLSF_FAVORITES;
 +              listflags |= SLSF_CATEGORIES;
 +              if(me.currentSortOrder < 0) { listflags |= SLSF_DESCENDING; }
 +              sethostcachesort(me.currentSortField, listflags);
 +      }
 +
 +      resorthostcache();
 +      if(mode >= REFRESHSERVERLIST_ASK)
 +              refreshhostcache(mode >= REFRESHSERVERLIST_RESET);
 +}
 +void XonoticServerList_focusEnter(entity me)
 +{
++      SUPER(XonoticServerList).focusEnter(me);
 +      if(time < me.nextRefreshTime)
 +      {
 +              //print("sorry, no refresh yet\n");
 +              return;
 +      }
 +      me.nextRefreshTime = time + 10;
 +      me.refreshServerList(me, REFRESHSERVERLIST_ASK);
 +}
 +
 +void XonoticServerList_draw(entity me)
 +{
 +      float i, found, owned;
 +
 +      if(_Nex_ExtResponseSystem_BannedServersNeedsRefresh)
 +      {
 +              if(!me.needsRefresh)
 +                      me.needsRefresh = 2;
 +              _Nex_ExtResponseSystem_BannedServersNeedsRefresh = 0;
 +      }
 +
 +      if(_Nex_ExtResponseSystem_PromotedServersNeedsRefresh)
 +      {
 +              if(!me.needsRefresh)
 +                      me.needsRefresh = 3;
 +              _Nex_ExtResponseSystem_PromotedServersNeedsRefresh = 0;
 +      }
 +
 +      if(_Nex_ExtResponseSystem_RecommendedServersNeedsRefresh)
 +      {
 +              if(!me.needsRefresh)
 +                      me.needsRefresh = 3;
 +              _Nex_ExtResponseSystem_RecommendedServersNeedsRefresh = 0;
 +      }
 +
 +      if(me.currentSortField == -1)
 +      {
 +              me.setSortOrder(me, SLIST_FIELD_PING, +1);
 +              me.refreshServerList(me, REFRESHSERVERLIST_RESET);
 +      }
 +      else if(me.needsRefresh == 1)
 +      {
 +              me.needsRefresh = 2; // delay by one frame to make sure "slist" has been executed
 +      }
 +      else if(me.needsRefresh == 2)
 +      {
 +              me.needsRefresh = 0;
 +              me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 +      }
 +      else if(me.needsRefresh == 3)
 +      {
 +              me.needsRefresh = 0;
 +              me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
 +      }
 +
 +      owned = ((me.selectedServer == me.ipAddressBox.text) && (me.ipAddressBox.text != ""));
 +
 +      for(i = 0; i < category_draw_count; ++i) { category_name[i] = -1; category_item[i] = -1; }
 +      category_draw_count = 0;
 +
 +      if(autocvar_menu_slist_categories >= 0) // if less than 0, don't even draw a category heading for favorites
 +      {
 +              float itemcount = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT);
 +              me.nItems = itemcount;
 +
 +              //float visible = floor(me.scrollPos / me.itemHeight);
 +              // ^ unfortunately no such optimization can be made-- we must process through the
 +              // entire list, otherwise there is no way to know which item is first in its category.
 +
 +              // binary search method suggested by div
 +              float x;
 +              float begin = 0;
 +              for(x = 1; x <= category_ent_count; ++x) {
 +                      float first = begin;
 +                      float last = (itemcount - 1);
 +                      if (first > last) {
 +                              // List is empty.
 +                              break;
 +                      }
 +                      float catf = gethostcachenumber(SLIST_FIELD_CATEGORY, first);
 +                      float catl = gethostcachenumber(SLIST_FIELD_CATEGORY, last);
 +                      if (catf > x) {
 +                              // The first one is already > x.
 +                              // Therefore, category x does not exist.
 +                              // Higher numbered categories do exist though.
 +                      } else if (catl < x) {
 +                              // The last one is < x.
 +                              // Thus this category - and any following -
 +                              // don't exist.
 +                              break;
 +                      } else if (catf == x) {
 +                              // Starts at first. This breaks the loop
 +                              // invariant in the binary search and thus has
 +                              // to be handled separately.
 +                              if(gethostcachenumber(SLIST_FIELD_CATEGORY, first) != x)
 +                                      error("Category mismatch I");
 +                              if(first > 0)
 +                                      if(gethostcachenumber(SLIST_FIELD_CATEGORY, first - 1) == x)
 +                                              error("Category mismatch II");
 +                              category_name[category_draw_count] = x;
 +                              category_item[category_draw_count] = first;
 +                              ++category_draw_count;
 +                              begin = first + 1;
 +                      } else {
 +                              // At this point, catf <= x < catl, thus
 +                              // catf < catl, thus first < last.
 +                              // INVARIANTS:
 +                              // last - first >= 1
 +                              // catf == gethostcachenumber(SLIST_FIELD_CATEGORY(first)
 +                              // catl == gethostcachenumber(SLIST_FIELD_CATEGORY(last)
 +                              // catf < x
 +                              // catl >= x
 +                              while (last - first > 1) {
 +                                      float middle = floor((first + last) / 2);
 +                                      // By loop condition, middle != first && middle != last.
 +                                      float cat = gethostcachenumber(SLIST_FIELD_CATEGORY, middle);
 +                                      if (cat >= x) {
 +                                              last = middle;
 +                                              catl = cat;
 +                                      } else {
 +                                              first = middle;
 +                                              catf = cat;
 +                                      }
 +                              }
 +                              if (catl == x) {
 +                                      if(gethostcachenumber(SLIST_FIELD_CATEGORY, last) != x)
 +                                              error("Category mismatch III");
 +                                      if(last > 0)
 +                                              if(gethostcachenumber(SLIST_FIELD_CATEGORY, last - 1) == x)
 +                                                      error("Category mismatch IV");
 +                                      category_name[category_draw_count] = x;
 +                                      category_item[category_draw_count] = last;
 +                                      ++category_draw_count;
 +                                      begin = last + 1; // already scanned through these, skip 'em
 +                              }
 +                              else
 +                                      begin = last; // already scanned through these, skip 'em
 +                      }
 +              }
 +              if(autocvar_menu_slist_categories_onlyifmultiple && (category_draw_count == 1))
 +              {
 +                      category_name[0] = -1;
 +                      category_item[0] = -1;
 +                      category_draw_count = 0;
 +                      me.nItems = itemcount;
 +              }
 +      }
 +      else { me.nItems = gethostcachevalue(SLIST_HOSTCACHEVIEWCOUNT); }
 +
 +      me.connectButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
 +      me.infoButton.disabled = ((me.nItems == 0) || !owned);
 +      me.favoriteButton.disabled = ((me.nItems == 0) && (me.ipAddressBox.text == ""));
 +
 +      found = 0;
 +      if(me.selectedServer)
 +      {
 +              for(i = 0; i < me.nItems; ++i)
 +              {
 +                      if(gethostcachestring(SLIST_FIELD_CNAME, i) == me.selectedServer)
 +                      {
 +                              me.selectedItem = i;
 +                              found = 1;
 +                              break;
 +                      }
 +              }
 +      }
 +      if(!found)
 +      {
 +              if(me.nItems > 0)
 +              {
 +                      if(me.selectedItem >= me.nItems)
 +                              me.selectedItem = me.nItems - 1;
 +                      if(me.selectedServer)
 +                              strunzone(me.selectedServer);
 +                      me.selectedServer = strzone(gethostcachestring(SLIST_FIELD_CNAME, me.selectedItem));
 +              }
 +      }
 +
 +      if(owned)
 +      {
 +              if(me.selectedServer != me.ipAddressBox.text)
 +              {
 +                      me.ipAddressBox.setText(me.ipAddressBox, me.selectedServer);
 +                      me.ipAddressBox.cursorPos = strlen(me.selectedServer);
 +                      me.ipAddressBoxFocused = -1;
 +              }
 +      }
 +
 +      if(me.ipAddressBoxFocused != me.ipAddressBox.focused)
 +      {
 +              if(me.ipAddressBox.focused || me.ipAddressBoxFocused < 0)
 +                      ServerList_Update_favoriteButton(NULL, me);
 +              me.ipAddressBoxFocused = me.ipAddressBox.focused;
 +      }
 +
 +      SUPER(XonoticServerList).draw(me);
 +}
 +void ServerList_PingSort_Click(entity btn, entity me)
 +{
 +      me.setSortOrder(me, SLIST_FIELD_PING, +1);
 +}
 +void ServerList_NameSort_Click(entity btn, entity me)
 +{
 +      me.setSortOrder(me, SLIST_FIELD_NAME, -1); // why?
 +}
 +void ServerList_MapSort_Click(entity btn, entity me)
 +{
 +      me.setSortOrder(me, SLIST_FIELD_MAP, -1); // why?
 +}
 +void ServerList_PlayerSort_Click(entity btn, entity me)
 +{
 +      me.setSortOrder(me, SLIST_FIELD_NUMHUMANS, -1);
 +}
 +void ServerList_TypeSort_Click(entity btn, entity me)
 +{
 +      string s, t;
 +      float i, m;
 +      s = me.filterString;
 +      m = strstrofs(s, ":", 0);
 +      if(m >= 0)
 +      {
 +              s = substring(s, 0, m);
 +              while(substring(s, m+1, 1) == " ") // skip spaces
 +                      ++m;
 +      }
 +      else
 +              s = "";
 +
 +      for(i = 1; ; i *= 2) // 20 modes ought to be enough for anyone
 +      {
 +              t = MapInfo_Type_ToString(i);
 +              if(i > 1)
 +                      if(t == "") // it repeats (default case)
 +                      {
 +                              // no type was found
 +                              // choose the first one
 +                              s = MapInfo_Type_ToString(1);
 +                              break;
 +                      }
 +              if(s == t)
 +              {
 +                      // the type was found
 +                      // choose the next one
 +                      s = MapInfo_Type_ToString(i * 2);
 +                      if(s == "")
 +                              s = MapInfo_Type_ToString(1);
 +                      break;
 +              }
 +      }
 +
 +      if(s != "")
 +              s = strcat(s, ":");
 +      s = strcat(s, substring(me.filterString, m+1, strlen(me.filterString) - m - 1));
 +
 +      me.controlledTextbox.setText(me.controlledTextbox, s);
 +      me.controlledTextbox.keyDown(me.controlledTextbox, K_END, 0, 0);
 +      me.controlledTextbox.keyUp(me.controlledTextbox, K_END, 0, 0);
 +      //ServerList_Filter_Change(me.controlledTextbox, me);
 +}
 +void ServerList_Filter_Change(entity box, entity me)
 +{
 +      if(me.filterString)
 +              strunzone(me.filterString);
 +      if(box.text != "")
 +              me.filterString = strzone(box.text);
 +      else
 +              me.filterString = string_null;
 +      me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 +
 +      me.ipAddressBox.setText(me.ipAddressBox, "");
 +      me.ipAddressBox.cursorPos = 0;
 +      me.ipAddressBoxFocused = -1;
 +}
 +void ServerList_Categories_Click(entity box, entity me)
 +{
 +      box.setChecked(box, autocvar_menu_slist_categories = !autocvar_menu_slist_categories);
 +      me.refreshServerList(me, REFRESHSERVERLIST_RESORT);
 +
 +      me.ipAddressBox.setText(me.ipAddressBox, "");
 +      me.ipAddressBox.cursorPos = 0;
 +      me.ipAddressBoxFocused = -1;
 +}
 +void ServerList_ShowEmpty_Click(entity box, entity me)
 +{
 +      box.setChecked(box, me.filterShowEmpty = !me.filterShowEmpty);
 +      me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 +
 +      me.ipAddressBox.setText(me.ipAddressBox, "");
 +      me.ipAddressBox.cursorPos = 0;
 +      me.ipAddressBoxFocused = -1;
 +}
 +void ServerList_ShowFull_Click(entity box, entity me)
 +{
 +      box.setChecked(box, me.filterShowFull = !me.filterShowFull);
 +      me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 +
 +      me.ipAddressBox.setText(me.ipAddressBox, "");
 +      me.ipAddressBox.cursorPos = 0;
 +      me.ipAddressBoxFocused = -1;
 +}
 +void XonoticServerList_setSortOrder(entity me, float fld, float direction)
 +{
 +      if(me.currentSortField == fld)
 +              direction = -me.currentSortOrder;
 +      me.currentSortOrder = direction;
 +      me.currentSortField = fld;
 +      me.sortButton1.forcePressed = (fld == SLIST_FIELD_PING);
 +      me.sortButton2.forcePressed = (fld == SLIST_FIELD_NAME);
 +      me.sortButton3.forcePressed = (fld == SLIST_FIELD_MAP);
 +      me.sortButton4.forcePressed = 0;
 +      me.sortButton5.forcePressed = (fld == SLIST_FIELD_NUMHUMANS);
 +      me.selectedItem = 0;
 +      if(me.selectedServer)
 +              strunzone(me.selectedServer);
 +      me.selectedServer = string_null;
 +      me.refreshServerList(me, REFRESHSERVERLIST_REFILTER);
 +}
 +void XonoticServerList_positionSortButton(entity me, entity btn, float theOrigin, float theSize, string theTitle, void(entity, entity) theFunc)
 +{
 +      vector originInLBSpace, sizeInLBSpace;
 +      originInLBSpace = eY * (-me.itemHeight);
 +      sizeInLBSpace = eY * me.itemHeight + eX * (1 - me.controlWidth);
 +
 +      vector originInDialogSpace, sizeInDialogSpace;
 +      originInDialogSpace = boxToGlobal(originInLBSpace, me.Container_origin, me.Container_size);
 +      sizeInDialogSpace = boxToGlobalSize(sizeInLBSpace, me.Container_size);
 +
 +      btn.Container_origin_x = originInDialogSpace.x + sizeInDialogSpace.x * theOrigin;
 +      btn.Container_size_x   =                         sizeInDialogSpace.x * theSize;
 +      btn.setText(btn, theTitle);
 +      btn.onClick = theFunc;
 +      btn.onClickEntity = me;
 +      btn.resized = 1;
 +}
 +void XonoticServerList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      SUPER(XonoticServerList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +
 +      me.realFontSize_y = me.fontSize / (absSize.y * me.itemHeight);
 +      me.realFontSize_x = me.fontSize / (absSize.x * (1 - me.controlWidth));
 +      me.realUpperMargin = 0.5 * (1 - me.realFontSize.y);
 +
 +      me.columnIconsOrigin = 0;
 +      me.columnIconsSize = me.realFontSize.x * 4 * me.iconsSizeFactor;
 +      me.columnPingSize = me.realFontSize.x * 3;
 +      me.columnMapSize = me.realFontSize.x * 10;
 +      me.columnTypeSize = me.realFontSize.x * 4;
 +      me.columnPlayersSize = me.realFontSize.x * 5;
 +      me.columnNameSize = 1 - me.columnPlayersSize - me.columnMapSize - me.columnPingSize - me.columnIconsSize - me.columnTypeSize - 5 * me.realFontSize.x;
 +      me.columnPingOrigin = me.columnIconsOrigin + me.columnIconsSize + me.realFontSize.x;
 +      me.columnNameOrigin = me.columnPingOrigin + me.columnPingSize + me.realFontSize.x;
 +      me.columnMapOrigin = me.columnNameOrigin + me.columnNameSize + me.realFontSize.x;
 +      me.columnTypeOrigin = me.columnMapOrigin + me.columnMapSize + me.realFontSize.x;
 +      me.columnPlayersOrigin = me.columnTypeOrigin + me.columnTypeSize + me.realFontSize.x;
 +
 +      me.positionSortButton(me, me.sortButton1, me.columnPingOrigin, me.columnPingSize, _("Ping"), ServerList_PingSort_Click);
 +      me.positionSortButton(me, me.sortButton2, me.columnNameOrigin, me.columnNameSize, _("Host name"), ServerList_NameSort_Click);
 +      me.positionSortButton(me, me.sortButton3, me.columnMapOrigin, me.columnMapSize, _("Map"), ServerList_MapSort_Click);
 +      me.positionSortButton(me, me.sortButton4, me.columnTypeOrigin, me.columnTypeSize, _("Type"), ServerList_TypeSort_Click);
 +      me.positionSortButton(me, me.sortButton5, me.columnPlayersOrigin, me.columnPlayersSize, _("Players"), ServerList_PlayerSort_Click);
 +
 +      float f;
 +      f = me.currentSortField;
 +      if(f >= 0)
 +      {
 +              me.currentSortField = -1;
 +              me.setSortOrder(me, f, me.currentSortOrder); // force resetting the sort order
 +      }
 +}
 +void ServerList_Connect_Click(entity btn, entity me)
 +{
 +      localcmd(sprintf("connect %s\n",
 +              ((me.ipAddressBox.text != "") ?
 +                      me.ipAddressBox.text : me.selectedServer
 +              )
 +      ));
 +}
 +void ServerList_Favorite_Click(entity btn, entity me)
 +{
 +      string ipstr;
 +      ipstr = netaddress_resolve(me.ipAddressBox.text, 26000);
 +      if(ipstr != "")
 +      {
++              m_play_click_sound(MENU_SOUND_SELECT);
 +              me.toggleFavorite(me, me.ipAddressBox.text);
 +              me.ipAddressBoxFocused = -1;
 +      }
 +}
 +void ServerList_Info_Click(entity btn, entity me)
 +{
 +      if (me.nItems != 0)
 +              main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
 +
 +      vector org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
 +      vector sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
 +      DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
 +}
 +void XonoticServerList_doubleClickListBoxItem(entity me, float i, vector where)
 +{
 +      ServerList_Connect_Click(NULL, me);
 +}
 +void XonoticServerList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      // layout: Ping, Server name, Map name, NP, TP, MP
 +      float p, q;
 +      float isv4, isv6;
 +      vector theColor;
 +      float theAlpha;
 +      float m, pure, freeslots, j, sflags;
 +      string s, typestr, versionstr, k, v, modname;
 +
 +      //printf("time: %f, i: %d, item: %d, nitems: %d\n", time, i, item, me.nItems);
 +
 +      vector oldscale = draw_scale;
 +      vector oldshift = draw_shift;
 +#define SET_YRANGE(start,end) \
 +      draw_scale = boxToGlobalSize(eX * 1 + eY * (end - start), oldscale); \
 +      draw_shift = boxToGlobal(eY * start, oldshift, oldscale);
 +
 +      for (j = 0; j < category_draw_count; ++j) {
 +              // Matches exactly the headings with increased height.
 +              if (i == category_item[j])
 +                      break;
 +      }
 +
 +      if (j < category_draw_count)
 +      {
 +              entity catent = RetrieveCategoryEnt(category_name[j]);
 +              if(catent)
 +              {
 +                      SET_YRANGE(
 +                              (me.categoriesHeight - 1) / (me.categoriesHeight + 1),
 +                              me.categoriesHeight / (me.categoriesHeight + 1)
 +                      );
 +                      draw_Text(
 +                              eY * me.realUpperMargin
 +                              +
 +#if 0
 +                              eX * (me.columnNameOrigin + (me.columnNameSize - draw_TextWidth(catent.cat_string, 0, me.realFontSize)) * 0.5),
 +                              catent.cat_string,
 +#else
 +                              eX * (me.columnNameOrigin),
 +                              strcat(catent.cat_string, ":"),
 +#endif
 +                              me.realFontSize,
 +                              SKINCOLOR_SERVERLIST_CATEGORY,
 +                              SKINALPHA_SERVERLIST_CATEGORY,
 +                              0
 +                      );
 +                      SET_YRANGE(me.categoriesHeight / (me.categoriesHeight + 1), 1);
 +              }
 +      }
 +
 +      if(isSelected)
 +              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 +
 +      s = gethostcachestring(SLIST_FIELD_QCSTATUS, i);
 +      m = tokenizebyseparator(s, ":");
 +      typestr = "";
 +      if(m >= 2)
 +      {
 +              typestr = argv(0);
 +              versionstr = argv(1);
 +      }
 +      freeslots = -1;
 +      sflags = -1;
 +      modname = "";
 +      pure = 0;
 +      for(j = 2; j < m; ++j)
 +      {
 +              if(argv(j) == "")
 +                      break;
 +              k = substring(argv(j), 0, 1);
 +              v = substring(argv(j), 1, -1);
 +              if(k == "P")
 +                      pure = stof(v);
 +              else if(k == "S")
 +                      freeslots = stof(v);
 +              else if(k == "F")
 +                      sflags = stof(v);
 +              else if(k == "M")
 +                      modname = v;
 +      }
 +
 +#ifdef COMPAT_NO_MOD_IS_XONOTIC
 +      if(modname == "")
 +              modname = "Xonotic";
 +#endif
 +
 +      /*
 +      SLIST_FIELD_MOD = gethostcacheindexforkey("mod");
 +      s = gethostcachestring(SLIST_FIELD_MOD, i);
 +      if(s != "data")
 +              if(modname == "Xonotic")
 +                      modname = s;
 +      */
 +
 +      // list the mods here on which the pure server check actually works
 +      if(modname != "Xonotic")
 +      if(modname != "InstaGib" || modname != "MinstaGib")
 +      if(modname != "CTS")
 +      if(modname != "NIX")
 +      if(modname != "NewToys")
 +              pure = 0;
 +
 +      if(gethostcachenumber(SLIST_FIELD_FREESLOTS, i) <= 0)
 +              theAlpha = SKINALPHA_SERVERLIST_FULL;
 +      else if(freeslots == 0)
 +              theAlpha = SKINALPHA_SERVERLIST_FULL; // g_maxplayers support
 +      else if (!gethostcachenumber(SLIST_FIELD_NUMHUMANS, i))
 +              theAlpha = SKINALPHA_SERVERLIST_EMPTY;
 +      else
 +              theAlpha = 1;
 +
 +      p = gethostcachenumber(SLIST_FIELD_PING, i);
 +      const float PING_LOW = 75;
 +      const float PING_MED = 200;
 +      const float PING_HIGH = 500;
 +      if(p < PING_LOW)
 +              theColor = SKINCOLOR_SERVERLIST_LOWPING + (SKINCOLOR_SERVERLIST_MEDPING - SKINCOLOR_SERVERLIST_LOWPING) * (p / PING_LOW);
 +      else if(p < PING_MED)
 +              theColor = SKINCOLOR_SERVERLIST_MEDPING + (SKINCOLOR_SERVERLIST_HIGHPING - SKINCOLOR_SERVERLIST_MEDPING) * ((p - PING_LOW) / (PING_MED - PING_LOW));
 +      else if(p < PING_HIGH)
 +      {
 +              theColor = SKINCOLOR_SERVERLIST_HIGHPING;
 +              theAlpha *= 1 + (SKINALPHA_SERVERLIST_HIGHPING - 1) * ((p - PING_MED) / (PING_HIGH - PING_MED));
 +      }
 +      else
 +      {
 +              theColor = eX;
 +              theAlpha *= SKINALPHA_SERVERLIST_HIGHPING;
 +      }
 +
 +      if(gethostcachenumber(SLIST_FIELD_ISFAVORITE, i))
 +      {
 +              theColor = theColor * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINCOLOR_SERVERLIST_FAVORITE * SKINALPHA_SERVERLIST_FAVORITE;
 +              theAlpha = theAlpha * (1 - SKINALPHA_SERVERLIST_FAVORITE) + SKINALPHA_SERVERLIST_FAVORITE;
 +      }
 +
 +      s = gethostcachestring(SLIST_FIELD_CNAME, i);
 +
 +      isv4 = isv6 = 0;
 +      if(substring(s, 0, 1) == "[")
 +      {
 +              isv6 = 1;
 +              me.seenIPv6 += 1;
 +      }
 +      else if(strstrofs("0123456789", substring(s, 0, 1), 0) >= 0)
 +      {
 +              isv4 = 1;
 +              me.seenIPv4 += 1;
 +      }
 +
 +      q = stof(substring(crypto_getencryptlevel(s), 0, 1));
 +      if((q <= 0 && cvar("crypto_aeslevel") >= 3) || (q >= 3 && cvar("crypto_aeslevel") <= 0))
 +      {
 +              theColor = SKINCOLOR_SERVERLIST_IMPOSSIBLE;
 +              theAlpha = SKINALPHA_SERVERLIST_IMPOSSIBLE;
 +      }
 +
 +      if(q == 1)
 +      {
 +              if(cvar("crypto_aeslevel") >= 2)
 +                      q |= 4;
 +      }
 +      if(q == 2)
 +      {
 +              if(cvar("crypto_aeslevel") >= 1)
 +                      q |= 4;
 +      }
 +      if(q == 3)
 +              q = 5;
 +      else if(q >= 3)
 +              q -= 2;
 +      // possible status:
 +      // 0: crypto off
 +      // 1: AES possible
 +      // 2: AES recommended but not available
 +      // 3: AES possible and will be used
 +      // 4: AES recommended and will be used
 +      // 5: AES required
 +
 +      // --------------
 +      //  RENDER ICONS
 +      // --------------
 +      vector iconSize = '0 0 0';
 +      iconSize_y = me.realFontSize.y * me.iconsSizeFactor;
 +      iconSize_x = me.realFontSize.x * me.iconsSizeFactor;
 +
 +      vector iconPos = '0 0 0';
 +      iconPos_x = (me.columnIconsSize - 3 * iconSize.x) * 0.5;
 +      iconPos_y = (1 - iconSize.y) * 0.5;
 +
 +      string n;
 +
 +      if (!(me.seenIPv4 && me.seenIPv6))
 +      {
 +              iconPos.x += iconSize.x * 0.5;
 +      }
 +      else if(me.seenIPv4 && me.seenIPv6)
 +      {
 +              n = string_null;
 +              if(isv6)
 +                      draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv6"), 0); // PRECACHE_PIC_MIPMAP
 +              else if(isv4)
 +                      draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_ipv4"), 0); // PRECACHE_PIC_MIPMAP
 +              if(n)
 +                      draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
 +              iconPos.x += iconSize.x;
 +      }
 +
 +      if(q > 0)
 +      {
 +              draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_aeslevel", ftos(q)), 0); // PRECACHE_PIC_MIPMAP
 +              draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
 +      }
 +      iconPos.x += iconSize.x;
 +
 +      if(modname == "Xonotic")
 +      {
 +              if(pure == 0)
 +              {
 +                      draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_pure1"), PRECACHE_PIC_MIPMAP);
 +                      draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
 +              }
 +      }
 +      else
 +      {
 +              draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_", modname), PRECACHE_PIC_MIPMAP);
 +              if(draw_PictureSize(n) == '0 0 0')
 +                      draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_mod_"), PRECACHE_PIC_MIPMAP);
 +              if(pure == 0)
 +                      draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
 +              else
 +                      draw_Picture(iconPos, n, iconSize, '1 1 1', SKINALPHA_SERVERLIST_ICON_NONPURE);
 +      }
 +      iconPos.x += iconSize.x;
 +
 +      if(sflags >= 0 && (sflags & SERVERFLAG_PLAYERSTATS))
 +      {
 +              draw_PreloadPictureWithFlags(n = strcat(SKINGFX_SERVERLIST_ICON, "_stats1"), 0); // PRECACHE_PIC_MIPMAP
 +              draw_Picture(iconPos, n, iconSize, '1 1 1', 1);
 +      }
 +      iconPos.x += iconSize.x;
 +
 +      // --------------
 +      //  RENDER TEXT
 +      // --------------
 +
 +      // ping
 +      s = ftos(p);
 +      draw_Text(me.realUpperMargin * eY + (me.columnPingOrigin + me.columnPingSize - draw_TextWidth(s, 0, me.realFontSize)) * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +
 +      // server name
 +      s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_NAME, i), me.columnNameSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin * eY + me.columnNameOrigin * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +
 +      // server map
 +      s = draw_TextShortenToWidth(gethostcachestring(SLIST_FIELD_MAP, i), me.columnMapSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin * eY + (me.columnMapOrigin + (me.columnMapSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +
 +      // server gametype
 +      s = draw_TextShortenToWidth(typestr, me.columnTypeSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin * eY + (me.columnTypeOrigin + (me.columnTypeSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +
 +      // server playercount
 +      s = strcat(ftos(gethostcachenumber(SLIST_FIELD_NUMHUMANS, i)), "/", ftos(gethostcachenumber(SLIST_FIELD_MAXPLAYERS, i)));
 +      draw_Text(me.realUpperMargin * eY + (me.columnPlayersOrigin + (me.columnPlayersSize - draw_TextWidth(s, 0, me.realFontSize)) * 0.5) * eX, s, me.realFontSize, theColor, theAlpha, 0);
 +}
 +
 +float XonoticServerList_keyDown(entity me, float scan, float ascii, float shift)
 +{
 +      vector org, sz;
 +
 +      org = boxToGlobal(eY * (me.selectedItem * me.itemHeight - me.scrollPos), me.origin, me.size);
 +      sz = boxToGlobalSize(eY * me.itemHeight + eX * (1 - me.controlWidth), me.size);
 +
 +      if(scan == K_ENTER || scan == K_KP_ENTER)
 +      {
 +              ServerList_Connect_Click(NULL, me);
 +              return 1;
 +      }
 +      else if(scan == K_MOUSE2 || scan == K_SPACE)
 +      {
 +              if(me.nItems != 0)
 +              {
++                      m_play_click_sound(MENU_SOUND_OPEN);
 +                      main.serverInfoDialog.loadServerInfo(main.serverInfoDialog, me.selectedItem);
 +                      DialogOpenButton_Click_withCoords(me, main.serverInfoDialog, org, sz);
 +                      return 1;
 +              }
 +              return 0;
 +      }
 +      else if(scan == K_INS || scan == K_MOUSE3 || scan == K_KP_INS)
 +      {
 +              if(me.nItems != 0)
 +              {
 +                      me.toggleFavorite(me, me.selectedServer);
 +                      me.ipAddressBoxFocused = -1;
 +                      return 1;
 +              }
 +              return 0;
 +      }
 +      else if(SUPER(XonoticServerList).keyDown(me, scan, ascii, shift))
 +              return 1;
 +      else if(!me.controlledTextbox)
 +              return 0;
 +      else
 +              return me.controlledTextbox.keyDown(me.controlledTextbox, scan, ascii, shift);
 +}
 +
 +float XonoticServerList_getTotalHeight(entity me) {
 +      float num_normal_rows = me.nItems;
 +      float num_headers = category_draw_count;
 +      return me.itemHeight * (num_normal_rows + me.categoriesHeight * num_headers);
 +}
 +float XonoticServerList_getItemAtPos(entity me, float pos) {
 +      pos = pos / me.itemHeight;
 +      float i;
 +      for (i = category_draw_count - 1; i >= 0; --i) {
 +              float itemidx = category_item[i];
 +              float itempos = i * me.categoriesHeight + category_item[i];
 +              if (pos >= itempos + me.categoriesHeight + 1)
 +                      return itemidx + 1 + floor(pos - (itempos + me.categoriesHeight + 1));
 +              if (pos >= itempos)
 +                      return itemidx;
 +      }
 +      // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
 +      return floor(pos);
 +}
 +float XonoticServerList_getItemStart(entity me, float item) {
 +      float i;
 +      for (i = category_draw_count - 1; i >= 0; --i) {
 +              float itemidx = category_item[i];
 +              float itempos = i * me.categoriesHeight + category_item[i];
 +              if (item >= itemidx + 1)
 +                      return (itempos + me.categoriesHeight + 1 + item - (itemidx + 1)) * me.itemHeight;
 +              if (item >= itemidx)
 +                      return itempos * me.itemHeight;
 +      }
 +      // No category matches? Note that category 0 is... 0. Therefore no headings exist at all.
 +      return item * me.itemHeight;
 +}
 +float XonoticServerList_getItemHeight(entity me, float item) {
 +      float i;
 +      for (i = 0; i < category_draw_count; ++i) {
 +              // Matches exactly the headings with increased height.
 +              if (item == category_item[i])
 +                      return me.itemHeight * (me.categoriesHeight + 1);
 +      }
 +      return me.itemHeight;
 +}
 +
 +#endif
index c0c05a8,0000000..9990c83
mode 100644,000000..100644
--- /dev/null
@@@ -1,196 -1,0 +1,199 @@@
-       if(scan == K_ENTER || scan == K_KP_ENTER) {
 +#ifdef INTERFACE
 +CLASS(XonoticSkinList) EXTENDS(XonoticListBox)
 +      METHOD(XonoticSkinList, configureXonoticSkinList, void(entity))
 +      ATTRIB(XonoticSkinList, rowsPerItem, float, 4)
 +      METHOD(XonoticSkinList, resizeNotify, void(entity, vector, vector, vector, vector))
 +      METHOD(XonoticSkinList, drawListBoxItem, void(entity, float, vector, float))
 +      METHOD(XonoticSkinList, getSkins, void(entity))
 +      METHOD(XonoticSkinList, setSkin, void(entity))
 +      METHOD(XonoticSkinList, loadCvars, void(entity))
 +      METHOD(XonoticSkinList, saveCvars, void(entity))
 +      METHOD(XonoticSkinList, skinParameter, string(entity, float, float))
 +      METHOD(XonoticSkinList, doubleClickListBoxItem, void(entity, float, vector))
 +      METHOD(XonoticSkinList, keyDown, float(entity, float, float, float))
 +      METHOD(XonoticSkinList, destroy, void(entity))
 +
 +      ATTRIB(XonoticSkinList, skinlist, float, -1)
 +      ATTRIB(XonoticSkinList, realFontSize, vector, '0 0 0')
 +      ATTRIB(XonoticSkinList, columnPreviewOrigin, float, 0)
 +      ATTRIB(XonoticSkinList, columnPreviewSize, float, 0)
 +      ATTRIB(XonoticSkinList, columnNameOrigin, float, 0)
 +      ATTRIB(XonoticSkinList, columnNameSize, float, 0)
 +      ATTRIB(XonoticSkinList, realUpperMargin1, float, 0)
 +      ATTRIB(XonoticSkinList, realUpperMargin2, float, 0)
 +      ATTRIB(XonoticSkinList, origin, vector, '0 0 0')
 +      ATTRIB(XonoticSkinList, itemAbsSize, vector, '0 0 0')
 +
 +      ATTRIB(XonoticSkinList, name, string, "skinselector")
 +ENDCLASS(XonoticSkinList)
 +
 +entity makeXonoticSkinList();
 +void SetSkin_Click(entity btn, entity me);
 +#endif
 +
 +#ifdef IMPLEMENTATION
 +
 +const float SKINPARM_NAME = 0;
 +const float SKINPARM_TITLE = 1;
 +const float SKINPARM_AUTHOR = 2;
 +const float SKINPARM_PREVIEW = 3;
 +const float SKINPARM_COUNT = 4;
 +
 +entity makeXonoticSkinList()
 +{
 +      entity me;
 +      me = spawnXonoticSkinList();
 +      me.configureXonoticSkinList(me);
 +      return me;
 +}
 +
 +void XonoticSkinList_configureXonoticSkinList(entity me)
 +{
 +      me.configureXonoticListBox(me);
 +      me.getSkins(me);
 +      me.loadCvars(me);
 +}
 +
 +void XonoticSkinList_loadCvars(entity me)
 +{
 +      string s;
 +      float i, n;
 +      s = cvar_string("menu_skin");
 +      n = me.nItems;
 +      for(i = 0; i < n; ++i)
 +      {
 +              if(me.skinParameter(me, i, SKINPARM_NAME) == s)
 +              {
 +                      me.selectedItem = i;
 +                      break;
 +              }
 +      }
 +}
 +
 +void XonoticSkinList_saveCvars(entity me)
 +{
 +      cvar_set("menu_skin", me.skinParameter(me, me.selectedItem, SKINPARM_NAME));
 +}
 +
 +string XonoticSkinList_skinParameter(entity me, float i, float key)
 +{
 +      return bufstr_get(me.skinlist, i * SKINPARM_COUNT + key);
 +}
 +
 +void XonoticSkinList_getSkins(entity me)
 +{
 +      float glob, buf, i, n, fh;
 +      string s;
 +
 +      buf = buf_create();
 +      glob = search_begin("gfx/menu/*/skinvalues.txt", true, true);
 +      if(glob < 0)
 +      {
 +              me.skinlist = buf;
 +              me.nItems = 0;
 +              return;
 +      }
 +
 +      n = search_getsize(glob);
 +      for(i = 0; i < n; ++i)
 +      {
 +              s = search_getfilename(glob, i);
 +              bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_NAME, substring(s, 9, strlen(s) - 24)); // the * part
 +              bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_TITLE, _("<TITLE>"));
 +              bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_AUTHOR, _("<AUTHOR>"));
 +              if(draw_PictureSize(strcat("/gfx/menu/", substring(s, 9, strlen(s) - 24), "/skinpreview")) == '0 0 0')
 +                      bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_PREVIEW, "nopreview_menuskin");
 +              else
 +                      bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_PREVIEW, strcat("/gfx/menu/", substring(s, 9, strlen(s) - 24), "/skinpreview"));
 +              fh = fopen(language_filename(s), FILE_READ);
 +              if(fh < 0)
 +              {
 +                      print("Warning: can't open skinvalues.txt file\n");
 +                      continue;
 +              }
 +              while((s = fgets(fh)))
 +              {
 +                      // these two are handled by skinlist.qc
 +                      if(substring(s, 0, 6) == "title ")
 +                              bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_TITLE, substring(s, 6, strlen(s) - 6));
 +                      else if(substring(s, 0, 7) == "author ")
 +                              bufstr_set(buf, i * SKINPARM_COUNT + SKINPARM_AUTHOR, substring(s, 7, strlen(s) - 7));
 +              }
 +              fclose(fh);
 +      }
 +
 +      search_end(glob);
 +
 +      me.skinlist = buf;
 +      me.nItems = n;
 +}
 +
 +void XonoticSkinList_destroy(entity me)
 +{
 +      buf_del(me.skinlist);
 +}
 +
 +void XonoticSkinList_resizeNotify(entity me, vector relOrigin, vector relSize, vector absOrigin, vector absSize)
 +{
 +      me.itemAbsSize = '0 0 0';
 +      SUPER(XonoticSkinList).resizeNotify(me, relOrigin, relSize, absOrigin, absSize);
 +
 +      me.realFontSize_y = me.fontSize / (me.itemAbsSize_y = (absSize.y * me.itemHeight));
 +      me.realFontSize_x = me.fontSize / (me.itemAbsSize_x = (absSize.x * (1 - me.controlWidth)));
 +      me.realUpperMargin1 = 0.5 * (1 - 2.5 * me.realFontSize.y);
 +      me.realUpperMargin2 = me.realUpperMargin1 + 1.5 * me.realFontSize.y;
 +
 +      me.columnPreviewOrigin = 0;
 +      me.columnPreviewSize = me.itemAbsSize.y / me.itemAbsSize.x * 4 / 3;
 +      me.columnNameOrigin = me.columnPreviewOrigin + me.columnPreviewSize + me.realFontSize.x;
 +      me.columnNameSize = 1 - me.columnPreviewSize - 2 * me.realFontSize.x;
 +}
 +
 +void XonoticSkinList_drawListBoxItem(entity me, float i, vector absSize, float isSelected)
 +{
 +      string s;
 +
 +      if(isSelected)
 +              draw_Fill('0 0 0', '1 1 0', SKINCOLOR_LISTBOX_SELECTED, SKINALPHA_LISTBOX_SELECTED);
 +
 +      s = me.skinParameter(me, i, SKINPARM_PREVIEW);
 +      draw_Picture(me.columnPreviewOrigin * eX, s, me.columnPreviewSize * eX + eY, '1 1 1', 1);
 +
 +      s = me.skinParameter(me, i, SKINPARM_TITLE);
 +      s = draw_TextShortenToWidth(s, me.columnNameSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin1 * eY + (me.columnNameOrigin + 0.00 * (me.columnNameSize - draw_TextWidth(s, 0, me.realFontSize))) * eX, s, me.realFontSize, SKINCOLOR_SKINLIST_TITLE, SKINALPHA_TEXT, 0);
 +
 +      s = me.skinParameter(me, i, SKINPARM_AUTHOR);
 +      s = draw_TextShortenToWidth(s, me.columnNameSize, 0, me.realFontSize);
 +      draw_Text(me.realUpperMargin2 * eY + (me.columnNameOrigin + 1.00 * (me.columnNameSize - draw_TextWidth(s, 0, me.realFontSize))) * eX, s, me.realFontSize, SKINCOLOR_SKINLIST_AUTHOR, SKINALPHA_TEXT, 0);
 +}
 +
 +void XonoticSkinList_setSkin(entity me)
 +{
 +      me.saveCvars(me);
 +      localcmd("\nmenu_restart\nmenu_cmd skinselect\n");
 +}
 +
 +void SetSkin_Click(entity btn, entity me)
 +{
 +      me.setSkin(me);
 +}
 +
 +void XonoticSkinList_doubleClickListBoxItem(entity me, float i, vector where)
 +{
++      m_play_click_sound(MENU_SOUND_EXECUTE);
 +      me.setSkin(me);
 +}
 +
 +float XonoticSkinList_keyDown(entity me, float scan, float ascii, float shift)
 +{
++      if(scan == K_ENTER || scan == K_KP_ENTER)
++      {
++              m_play_click_sound(MENU_SOUND_EXECUTE);
 +              me.setSkin(me);
 +              return 1;
 +      }
 +      else
 +              return SUPER(XonoticSkinList).keyDown(me, scan, ascii, shift);
 +}
 +#endif
Simple merge
Simple merge
Simple merge
@@@ -43,139 -35,4 +43,142 @@@ void timeout_handler_think()
  void CommonCommand_macro_write_aliases(float fh);
  
  // keep track of the next token to use for argc
 -float next_token;
 +float next_token;
 +
 +// select the proper prefix for usage and other messages
 +string GetCommandPrefix(entity caller);
 +
 +// if client return player nickname, or if server return admin nickname
 +string GetCallerName(entity caller);
 +
++// verify that the client provided is acceptable for kicking
++float VerifyKickableEntity(entity client);
++
 +// verify that the client provided is acceptable for use
 +float VerifyClientEntity(entity client, float must_be_real, float must_be_bots);
 +
 +// if the client is not acceptable, return a string to be used for error messages
 +string GetClientErrorString(float clienterror, string original_input);
 +
 +// is this entity number even in the possible range of entities?
 +float VerifyClientNumber(float tmp_number);
 +
 +entity GetIndexedEntity(float argc, float start_index);
 +
 +// find a player which matches the input string, and return their entity
 +entity GetFilteredEntity(string input);
 +
 +// same thing, but instead return their edict number
 +float GetFilteredNumber(string input);
 +
 +// switch between sprint and print depending on whether the receiver is the server or a player
 +void print_to(entity to, string input);
 +
 +// ==========================================
 +//  Supporting functions for common commands
 +// ==========================================
 +
 +// used by CommonCommand_timeout() and CommonCommand_timein() to handle game pausing and messaging and such.
 +void timeout_handler_reset();
 +
 +void timeout_handler_think();
 +
 +// ===================================================
 +//  Common commands used in both sv_cmd.qc and cmd.qc
 +// ===================================================
 +
 +void CommonCommand_cvar_changes(float request, entity caller);
 +
 +void CommonCommand_cvar_purechanges(float request, entity caller);
 +
 +void CommonCommand_info(float request, entity caller, float argc);
 +
 +void CommonCommand_ladder(float request, entity caller);
 +
 +void CommonCommand_lsmaps(float request, entity caller);
 +
 +void CommonCommand_printmaplist(float request, entity caller);
 +
 +void CommonCommand_rankings(float request, entity caller);
 +
 +void CommonCommand_records(float request, entity caller);
 +
 +void CommonCommand_teamstatus(float request, entity caller);
 +
 +void CommonCommand_time(float request, entity caller);
 +
 +void CommonCommand_timein(float request, entity caller);
 +
 +void CommonCommand_timeout(float request, entity caller);
 +
 +void CommonCommand_who(float request, entity caller, float argc);
 +
 +
 +// ==================================
 +//  Macro system for common commands
 +// ==================================
 +
 +// Do not hard code aliases for these, instead create them in commands.cfg... also: keep in alphabetical order, please ;)
 +#define COMMON_COMMANDS(request,caller,arguments,command) \
 +      COMMON_COMMAND("cvar_changes", CommonCommand_cvar_changes(request, caller), "Prints a list of all changed server cvars") \
 +      COMMON_COMMAND("cvar_purechanges", CommonCommand_cvar_purechanges(request, caller), "Prints a list of all changed gameplay cvars") \
 +      COMMON_COMMAND("info", CommonCommand_info(request, caller, arguments), "Request for unique server information set up by admin") \
 +      COMMON_COMMAND("ladder", CommonCommand_ladder(request, caller), "Get information about top players if supported") \
 +      COMMON_COMMAND("lsmaps", CommonCommand_lsmaps(request, caller), "List maps which can be used with the current game mode") \
 +      COMMON_COMMAND("printmaplist", CommonCommand_printmaplist(request, caller), "Display full server maplist reply") \
 +      COMMON_COMMAND("rankings", CommonCommand_rankings(request, caller), "Print information about rankings") \
 +      COMMON_COMMAND("records", CommonCommand_records(request, caller), "List top 10 records for the current map") \
 +      COMMON_COMMAND("teamstatus", CommonCommand_teamstatus(request, caller), "Show information about player and team scores") \
 +      COMMON_COMMAND("time", CommonCommand_time(request, caller), "Print different formats/readouts of time") \
 +      COMMON_COMMAND("timein", CommonCommand_timein(request, caller), "Resume the game from being paused with a timeout") \
 +      COMMON_COMMAND("timeout", CommonCommand_timeout(request, caller), "Call a timeout which pauses the game for certain amount of time unless unpaused") \
 +      COMMON_COMMAND("vote", VoteCommand(request, caller, arguments, command), "Request an action to be voted upon by players") \
 +      COMMON_COMMAND("who", CommonCommand_who(request, caller, arguments), "Display detailed client information about all players") \
 +      /* nothing */
 +
 +void CommonCommand_macro_help(entity caller)
 +{
 +      #define COMMON_COMMAND(name,function,description) \
 +              { print_to(caller, strcat("  ^2", name, "^7: ", description)); }
 +
 +      COMMON_COMMANDS(0, caller, 0, "");
 +      #undef COMMON_COMMAND
 +
 +      return;
 +}
 +
 +float CommonCommand_macro_command(float argc, entity caller, string command)
 +{
 +      #define COMMON_COMMAND(name,function,description) \
 +              { if(name == strtolower(argv(0))) { function; return true; } }
 +
 +      COMMON_COMMANDS(CMD_REQUEST_COMMAND, caller, argc, command);
 +      #undef COMMON_COMMAND
 +
 +      return false;
 +}
 +
 +float CommonCommand_macro_usage(float argc, entity caller)
 +{
 +      #define COMMON_COMMAND(name,function,description) \
 +              { if(name == strtolower(argv(1))) { function; return true; } }
 +
 +      COMMON_COMMANDS(CMD_REQUEST_USAGE, caller, argc, "");
 +      #undef COMMON_COMMAND
 +
 +      return false;
 +}
 +
 +void CommonCommand_macro_write_aliases(float fh)
 +{
 +      #define COMMON_COMMAND(name,function,description) \
 +              { CMD_Write_Alias("qc_cmd_svcmd", name, description); }
 +
 +      COMMON_COMMANDS(0, world, 0, "");
 +      #undef COMMON_COMMAND
 +
 +      return;
 +}
 +
 +
 +#endif
Simple merge
@@@ -470,9 -457,9 +470,9 @@@ float Ban_MaybeEnforceBan(entity client
  float Ban_MaybeEnforceBanOnce(entity client)
  {
        if(client.ban_checked)
 -              return FALSE;
 -      client.ban_checked = TRUE;
 +              return false;
 +      client.ban_checked = true;
-       return Ban_MaybeEnforceBan(self);
+       return Ban_MaybeEnforceBan(client);
  }
  
  string Ban_Enforce(float i, string reason)
index 0c8c105,0000000..7cebe9c
mode 100644,000000..100644
--- /dev/null
@@@ -1,684 -1,0 +1,687 @@@
 +#ifndef MISCFUNCTIONS_H
 +#define MISCFUNCTIONS_H
 +
 +#include "t_items.qh"
 +
 +#include "mutators/base.qh"
 +#include "mutators/gamemode_race.qh"
 +
 +#include "../common/constants.qh"
 +#include "../common/mapinfo.qh"
 +
 +#ifdef RELEASE
 +#define cvar_string_normal builtin_cvar_string
 +#define cvar_normal builtin_cvar
 +#else
 +string cvar_string_normal(string n)
 +{
 +      if (!(cvar_type(n) & 1))
 +              backtrace(strcat("Attempt to access undefined cvar: ", n));
 +      return builtin_cvar_string(n);
 +}
 +
 +float cvar_normal(string n)
 +{
 +      return stof(cvar_string_normal(n));
 +}
 +#endif
 +#define cvar_set_normal builtin_cvar_set
 +
 +.vector dropped_origin;
 +.void(void) uncustomizeentityforclient;
 +.float uncustomizeentityforclient_set;
 +.float nottargeted;
 +
 +
 +float DistributeEvenly_amount;
 +float DistributeEvenly_totalweight;
 +var void remove(entity e);
 +void objerror(string s);
 +void droptofloor();
 +void() spawnfunc_info_player_deathmatch; // needed for the other spawnpoints
 +void() spawnpoint_use;
 +void() SUB_Remove;
 +
 +void attach_sameorigin(entity e, entity to, string tag);
 +
 +void crosshair_trace(entity pl);
 +
 +void crosshair_trace_plusvisibletriggers(entity pl);
 +
 +void detach_sameorigin(entity e);
 +
 +void follow_sameorigin(entity e, entity to);
 +
 +string formatmessage(string msg);
 +
 +void GameLogEcho(string s);
 +
 +void GameLogInit();
 +
 +void GameLogClose();
 +
 +void GetCvars(float f);
 +
 +string GetMapname();
 +
 +float isPushable(entity e);
 +
 +float LostMovetypeFollow(entity ent);
 +
 +float MoveToRandomMapLocation(entity e, float goodcontents, float badcontents, float badsurfaceflags, float attempts, float maxaboveground, float minviewdistance);
 +
 +string NearestLocation(vector p);
 +
 +void play2(entity e, string filename);
 +
 +string playername(entity p);
 +
 +void precache();
 +
 +void remove_safely(entity e);
 +
 +void remove_unsafely(entity e);
 +
 +void SetMovetypeFollow(entity ent, entity e);
 +
 +vector shotorg_adjust_values(vector vecs, float y_is_right, float visual, float algn);
 +
 +void soundto(float dest, entity e, float chan, string samp, float vol, float atten);
 +
 +void stopsound(entity e, float chan);
 +
 +float tracebox_hits_box(vector start, vector mi, vector ma, vector end, vector thmi, vector thma);
 +
 +void traceline_antilag (entity source, vector v1, vector v2, float nomonst, entity forent, float lag);
 +
 +void WarpZone_crosshair_trace(entity pl);
 +
 +void WarpZone_traceline_antilag (entity source, vector v1, vector v2, float nomonst, entity forent, float lag);
 +
 +
 +#define IFTARGETED if(!self.nottargeted && self.targetname != "")
 +
 +#define ITEM_TOUCH_NEEDKILL() (((trace_dpstartcontents | trace_dphitcontents) & DPCONTENTS_NODROP) || (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY))
 +#define ITEM_DAMAGE_NEEDKILL(dt) (((dt) == DEATH_HURTTRIGGER) || ((dt) == DEATH_SLIME) || ((dt) == DEATH_LAVA) || ((dt) == DEATH_SWAMP))
 +
 +#define PROJECTILE_TOUCH if(WarpZone_Projectile_Touch()) return
 +
 +const string STR_PLAYER = "player";
 +const string STR_SPECTATOR = "spectator";
 +const string STR_OBSERVER = "observer";
 +
 +#define IS_PLAYER(v)                  (v.classname == STR_PLAYER)
 +#define IS_SPEC(v)                            (v.classname == STR_SPECTATOR)
 +#define IS_OBSERVER(v)                        (v.classname == STR_OBSERVER)
 +#define IS_CLIENT(v)                  (v.flags & FL_CLIENT)
 +#define IS_BOT_CLIENT(v)              (clienttype(v) == CLIENTTYPE_BOT)
 +#define IS_REAL_CLIENT(v)             (clienttype(v) == CLIENTTYPE_REAL)
 +#define IS_NOT_A_CLIENT(v)            (clienttype(v) == CLIENTTYPE_NOTACLIENT)
 +
 +#define FOR_EACH_CLIENTSLOT(v) for(v = world; (v = nextent(v)) && (num_for_edict(v) <= maxclients); )
 +#define FOR_EACH_CLIENT(v) FOR_EACH_CLIENTSLOT(v) if(IS_CLIENT(v))
 +#define FOR_EACH_REALCLIENT(v) FOR_EACH_CLIENT(v) if(IS_REAL_CLIENT(v))
 +
 +#define FOR_EACH_PLAYER(v) FOR_EACH_CLIENT(v) if(IS_PLAYER(v))
 +#define FOR_EACH_SPEC(v) FOR_EACH_CLIENT(v) if (!IS_PLAYER(v)) // Samual: shouldn't this be IS_SPEC(v)? and rather create a separate macro to include observers too
 +#define FOR_EACH_REALPLAYER(v) FOR_EACH_REALCLIENT(v) if(IS_PLAYER(v))
 +
 +#define FOR_EACH_MONSTER(v) for(v = world; (v = findflags(v, flags, FL_MONSTER)) != world; )
 +
 +#define CENTER_OR_VIEWOFS(ent) (ent.origin + (IS_PLAYER(ent) ? ent.view_ofs : ((ent.mins + ent.maxs) * 0.5)))
 +
 +// copies a string to a tempstring (so one can strunzone it)
 +string strcat1(string s) = #115; // FRIK_FILE
 +
 +float logfile_open;
 +float logfile;
 +
 +#define strstr strstrofs
 +/*
 +// NOTE: DO NOT USE THIS FUNCTION TOO OFTEN.
 +// IT WILL MOST PROBABLY DESTROY _ALL_ OTHER TEMP
 +// STRINGS AND TAKE QUITE LONG. haystack and needle MUST
 +// BE CONSTANT OR strzoneD!
 +float strstr(string haystack, string needle, float offset)
 +{
 +      float len, endpos;
 +      string found;
 +      len = strlen(needle);
 +      endpos = strlen(haystack) - len;
 +      while(offset <= endpos)
 +      {
 +              found = substring(haystack, offset, len);
 +              if(found == needle)
 +                      return offset;
 +              offset = offset + 1;
 +      }
 +      return -1;
 +}
 +*/
 +
 +const float NUM_NEAREST_ENTITIES = 4;
 +entity nearest_entity[NUM_NEAREST_ENTITIES];
 +float nearest_length[NUM_NEAREST_ENTITIES];
 +
 +
 +//#NO AUTOCVARS START
 +
 +float g_pickup_shells;
 +float g_pickup_shells_max;
 +float g_pickup_nails;
 +float g_pickup_nails_max;
 +float g_pickup_rockets;
 +float g_pickup_rockets_max;
 +float g_pickup_cells;
 +float g_pickup_cells_max;
 +float g_pickup_plasma;
 +float g_pickup_plasma_max;
 +float g_pickup_fuel;
 +float g_pickup_fuel_jetpack;
 +float g_pickup_fuel_max;
 +float g_pickup_armorsmall;
 +float g_pickup_armorsmall_max;
 +float g_pickup_armorsmall_anyway;
 +float g_pickup_armormedium;
 +float g_pickup_armormedium_max;
 +float g_pickup_armormedium_anyway;
 +float g_pickup_armorbig;
 +float g_pickup_armorbig_max;
 +float g_pickup_armorbig_anyway;
 +float g_pickup_armorlarge;
 +float g_pickup_armorlarge_max;
 +float g_pickup_armorlarge_anyway;
 +float g_pickup_healthsmall;
 +float g_pickup_healthsmall_max;
 +float g_pickup_healthsmall_anyway;
 +float g_pickup_healthmedium;
 +float g_pickup_healthmedium_max;
 +float g_pickup_healthmedium_anyway;
 +float g_pickup_healthlarge;
 +float g_pickup_healthlarge_max;
 +float g_pickup_healthlarge_anyway;
 +float g_pickup_healthmega;
 +float g_pickup_healthmega_max;
 +float g_pickup_healthmega_anyway;
 +float g_pickup_ammo_anyway;
 +float g_pickup_weapons_anyway;
 +float g_weaponarena;
 +WepSet g_weaponarena_weapons;
 +float g_weaponarena_random;
 +float g_weaponarena_random_with_blaster;
 +string g_weaponarena_list;
 +float g_weaponspeedfactor;
 +float g_weaponratefactor;
 +float g_weapondamagefactor;
 +float g_weaponforcefactor;
 +float g_weaponspreadfactor;
 +
 +WepSet start_weapons;
 +WepSet start_weapons_default;
 +WepSet start_weapons_defaultmask;
 +int start_items;
 +float start_ammo_shells;
 +float start_ammo_nails;
 +float start_ammo_rockets;
 +float start_ammo_cells;
 +float start_ammo_plasma;
 +float start_ammo_fuel;
 +float start_health;
 +float start_armorvalue;
 +WepSet warmup_start_weapons;
 +WepSet warmup_start_weapons_default;
 +WepSet warmup_start_weapons_defaultmask;
 +#define WARMUP_START_WEAPONS ((g_warmup_allguns == 1) ? (warmup_start_weapons & (weaponsInMap | start_weapons)) : warmup_start_weapons)
 +float warmup_start_ammo_shells;
 +float warmup_start_ammo_nails;
 +float warmup_start_ammo_rockets;
 +float warmup_start_ammo_cells;
 +float warmup_start_ammo_plasma;
 +float warmup_start_ammo_fuel;
 +float warmup_start_health;
 +float warmup_start_armorvalue;
 +float g_weapon_stay;
 +
 +float want_weapon(entity weaponinfo, float allguns) // WEAPONTODO: what still needs done?
 +{
 +      int i = weaponinfo.weapon;
 +      int d = 0;
 +
 +      if (!i)
 +              return 0;
 +
 +      if (g_lms || g_ca || allguns)
 +      {
 +              if(weaponinfo.spawnflags & WEP_FLAG_NORMAL)
 +                      d = true;
 +              else
 +                      d = false;
 +      }
 +      else if (g_cts)
 +              d = (i == WEP_SHOTGUN);
 +      else if (g_nexball)
 +              d = 0; // weapon is set a few lines later
 +      else
 +              d = !(!weaponinfo.weaponstart);
 +
 +      if(g_grappling_hook) // if possible, redirect off-hand hook to on-hand hook
 +              d |= (i == WEP_HOOK);
 +      if(!g_cts && (weaponinfo.spawnflags & WEP_FLAG_MUTATORBLOCKED)) // never default mutator blocked guns
 +              d = 0;
 +
 +      float t = weaponinfo.weaponstartoverride;
 +
 +      //print(strcat("want_weapon: ", weaponinfo.netname, " - d: ", ftos(d), ", t: ", ftos(t), ". \n"));
 +
 +      // bit order in t:
 +      // 1: want or not
 +      // 2: is default?
 +      // 4: is set by default?
 +      if(t < 0)
 +              t = 4 | (3 * d);
 +      else
 +              t |= (2 * d);
 +
 +      return t;
 +}
 +
 +void readplayerstartcvars()
 +{
 +      entity e;
 +      float i, j, t;
 +      string s;
 +
 +      // initialize starting values for players
 +      start_weapons = '0 0 0';
 +      start_weapons_default = '0 0 0';
 +      start_weapons_defaultmask = '0 0 0';
 +      start_items =&nb