// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file  d_netcmd.c
/// \brief host/client network commands
///        commands are executed through the command buffer
///	       like console commands, other miscellaneous commands (at the end)

#include "d_player.h"
#include "doomdef.h"

#include "console.h"
#include "command.h"
#include "i_time.h"
#include "i_gamepad.h"
#include "i_system.h"
#include "g_game.h"
#include "hu_stuff.h"
#include "m_emotes.h"
#include "g_input.h"
#include "m_menu.h"
#include "p_mobj.h"
#include "r_local.h"
#include "r_skins.h"
#include "p_local.h"
#include "p_setup.h"
#include "s_sound.h"
#include "i_sound.h"
#include "i_net.h"
#include "m_misc.h"
#include "am_map.h"
#include "byteptr.h"
#include "d_netfil.h"
#include "p_spec.h"
#include "m_cheat.h"
#include "d_clisrv.h"
#include "d_net.h"
#include "screen.h"
#include "v_video.h"
#include "d_main.h"
#include "m_random.h"
#include "f_finale.h"
#include "filesrch.h"
#include "mserv.h"
#include "z_zone.h"
#include "lua_script.h"
#include "lua_hook.h"
#include "m_cond.h"
#include "m_anigif.h"

// SRB2kart
#include "k_kart.h"
#include "k_battle.h"
#include "k_pwrlv.h"
#include "y_inter.h"
#include "k_color.h"
#include "k_hud.h"
#include "k_grandprix.h"
#include "k_boss.h"
#include "k_follower.h"
#include "doomstat.h"
#include "deh_tables.h"
#include "m_perfstats.h"
#include "g_party.h"
#include "k_specialstage.h"
#include "k_items.h"
#include "k_itemlist.hpp"

#define CV_RESTRICT CV_NETVAR

#ifdef HAVE_DISCORDRPC
#include "discord.h"
#endif

// ------
// protos
// ------

static void Got_NameAndColor(UINT8 **cp, INT32 playernum);
static void Got_WeaponPref(UINT8 **cp, INT32 playernum);
static void Got_PowerLevel(UINT8 **cp, INT32 playernum);
static void Got_PartyInvite(UINT8 **cp, INT32 playernum);
static void Got_AcceptPartyInvite(UINT8 **cp, INT32 playernum);
static void Got_CancelPartyInvite(UINT8 **cp, INT32 playernum);
static void Got_LeaveParty(UINT8 **cp, INT32 playernum);
static void Got_Mapcmd(UINT8 **cp, INT32 playernum);
static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum);
static void Got_SetupVotecmd(UINT8 **cp, INT32 playernum);
static void Got_ModifyVotecmd(UINT8 **cp, INT32 playernum);
static void Got_PickVotecmd(UINT8 **cp, INT32 playernum);
static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum);
static void Got_Addfilecmd(UINT8 **cp, INT32 playernum);
static void Got_Pause(UINT8 **cp, INT32 playernum);
static void Got_Respawn(UINT8 **cp, INT32 playernum);
static void Got_RandomSeed(UINT8 **cp, INT32 playernum);
static void Got_RunSOCcmd(UINT8 **cp, INT32 playernum);
static void Got_Teamchange(UINT8 **cp, INT32 playernum);
static void Got_Clearscores(UINT8 **cp, INT32 playernum);
static void Got_DiscordInfo(UINT8 **cp, INT32 playernum);
static void Got_ScheduleTaskcmd(UINT8 **cp, INT32 playernum);
static void Got_ScheduleClearcmd(UINT8 **cp, INT32 playernum);
static void Got_Automatecmd(UINT8 **cp, INT32 playernum);
static void Got_Cheat(UINT8 **cp, INT32 playernum);
static void Command_ScoreboardAdd(void);
static void Command_ScoreboardClear(void);

static void PointLimit_OnChange(void);
static void TimeLimit_OnChange(void);
static void NumLaps_OnChange(void);
static void Mute_OnChange(void);

static void AutoBalance_OnChange(void);
static void TeamScramble_OnChange(void);

static void NetTimeout_OnChange(void);
static void JoinTimeout_OnChange(void);

static void Lagless_OnChange (void);

static void Gravity_OnChange(void);
static void ForceSkin_OnChange(void);

static void Name_OnChange(void);
static void Name2_OnChange(void);
static void Name3_OnChange(void);
static void Name4_OnChange(void);
static void Skin_OnChange(void);
static void Skin2_OnChange(void);
static void Skin3_OnChange(void);
static void Skin4_OnChange(void);

static void Follower_OnChange(void);
static void Follower2_OnChange(void);
static void Follower3_OnChange(void);
static void Follower4_OnChange(void);
static void Followercolor_OnChange(void);
static void Followercolor2_OnChange(void);
static void Followercolor3_OnChange(void);
static void Followercolor4_OnChange(void);

static void Voice_OnChange(void);
static void Voice2_OnChange(void);
static void Voice3_OnChange(void);
static void Voice4_OnChange(void);

static void Color_OnChange(void);
static void Color2_OnChange(void);
static void Color3_OnChange(void);
static void Color4_OnChange(void);
static void DummyConsvar_OnChange(void);
static void SoundTest_OnChange(void);

static void KartFrantic_OnChange(void);
static void KartSpeed_OnChange(void);
static void KartBattleSpeed_OnChange(void);
static void KartEncore_OnChange(void);
static void KartComeback_OnChange(void);
static void KartEliminateLast_OnChange(void);
static void KartRings_OnChange(void);
static void KartPurpleDrift_OnChange(void);
static void KartStacking_OnChange(void);
static void KartChaining_OnChange(void);
static void KartSlipdash_OnChange(void);
static void KartSlopeBoost_OnChange(void);
static void KartDrafting_OnChange(void);
static void KartAirDrop_OnChange(void);
static void KartAirThrust_OnChange(void);
static void KartRecoveryDash_OnChange(void);
static void KartWaterSkipLock_OnChange(void);
static void KartItemLitter_OnChange(void);
static void KartItemPush_OnChange(void);
static void KartAntiBump_OnChange(void);
static void KartItemBreaker_OnChange(void);
static void KartBumpSpark_OnChange(void);
static void KartItemList_OnChange(void);

static void Schedule_OnChange(void);

static void Command_Playdemo_f(void);
static void Command_Timedemo_f(void);
static void Command_Stopdemo_f(void);
static void Command_StartMovie_f(void);
static void Command_StopMovie_f(void);
static void Command_Map_f(void);
static void Command_RandomMap(void);
static void Command_RestartLevel(void);
static void Command_ResetCamera_f(void);

static void Command_View_f (void);
static void Command_SetViews_f(void);

static void Command_Invite_f(void);
static void Command_CancelInvite_f(void);
static void Command_AcceptInvite_f(void);
static void Command_RejectInvite_f(void);
static void Command_LeaveParty_f(void);

static void Command_Addfilelocal(void);
static void Command_Addfile(void);
static void Command_ListWADS_f(void);
static void Command_LocateLump_f(void);
static void Command_ListDoomednums_f(void);
static void Command_RunSOC(void);
static void Command_Pause(void);

static void Command_Respawn(void);
static void Command_Respawn2(void);
static void Command_Respawn3(void);
static void Command_Respawn4(void);

static void Command_ReplayMarker(void);

static void Command_Version_f(void);
#ifdef UPDATE_ALERT
static void Command_ModDetails_f(void);
#endif
static void Command_ShowGametype_f(void);
FUNCNORETURN static ATTRNORETURN void Command_Quit_f(void);
static void Command_Playintro_f(void);

static void Command_Displayplayer_f(void);

static void Command_ExitLevel_f(void);
static void Command_Showmap_f(void);
static void Command_Maphash_f(void);

static void Command_Teamchange_f(void);
static void Command_Teamchange2_f(void);
static void Command_Teamchange3_f(void);
static void Command_Teamchange4_f(void);

static void Command_Restat(void);
static void Command_Restat2(void);
static void Command_Restat3(void);
static void Command_Restat4(void);

static void Command_ServerTeamChange_f(void);

static void Command_Clearscores_f(void);

// Remote Administration
static void Command_Changepassword_f(void);
static void Command_Clearpassword_f(void);
static void Command_Login_f(void);
static void Got_Verification(UINT8 **cp, INT32 playernum);
static void Got_Removal(UINT8 **cp, INT32 playernum);
static void Command_Verify_f(void);
static void Command_RemoveAdmin_f(void);
static void Command_MotD_f(void);
static void Got_MotD_f(UINT8 **cp, INT32 playernum);

static void Command_ShowScores_f(void);
static void Command_ShowTime_f(void);

static void Command_Isgamemodified_f(void);
static void Command_Cheats_f(void);

static void Command_ListSkins(void);
static void Command_ListVoices(void);

#ifdef _DEBUG
static void Command_Togglemodified_f(void);
static void Command_Archivetest_f(void);
#endif

static void Command_KartGiveItem_f(void);

static void Command_Schedule_Add(void);
static void Command_Schedule_Clear(void);
static void Command_Schedule_List(void);

static void Command_Automate_Set(void);

static void Command_PlaySound(void);

static void Got_PlaySound(UINT8 **cp, INT32 playernum);

// =========================================================================
//                           CLIENT VARIABLES
// =========================================================================

static CV_PossibleValue_t usemouse_cons_t[] = {{0, "Off"}, {1, "On"}, {2, "Force"}, {0, NULL}};

#ifdef LJOYSTICK
static CV_PossibleValue_t joyport_cons_t[] = {{1, "/dev/js0"}, {2, "/dev/js1"}, {3, "/dev/js2"},
	{4, "/dev/js3"}, {0, NULL}};
#else
// accept whatever value - it is in fact the joystick device number
static CV_PossibleValue_t usejoystick_cons_t[] = {{0, "MIN"}, {MAXGAMEPADS, "MAX"}, {0, NULL}};
#endif

static CV_PossibleValue_t teamscramble_cons_t[] = {{0, "Off"}, {1, "Random"}, {2, "Points"}, {0, NULL}};

static CV_PossibleValue_t startingliveslimit_cons_t[] = {{1, "MIN"}, {99, "MAX"}, {0, NULL}};

static CV_PossibleValue_t sleeping_cons_t[] = {{0, "MIN"}, {1000/TICRATE, "MAX"}, {0, NULL}};

static CV_PossibleValue_t pause_cons_t[] = {{0, "Server"}, {1, "All"}, {0, NULL}};

static consvar_t cv_dummyconsvar = CVAR_INIT ("dummyconsvar", "Off", CV_CALL|CV_NOSHOWHELP, CV_OnOff, DummyConsvar_OnChange);

consvar_t cv_restrictskinchange = CVAR_INIT ("restrictskinchange", "No", CV_NETVAR|CV_CHEAT, CV_YesNo, NULL);
consvar_t cv_allowteamchange = CVAR_INIT ("allowteamchange", "Yes", CV_NETVAR, CV_YesNo, NULL);

#ifdef DEVELOP
	// change the default value in doomdef.h (so it affects release builds too)
	consvar_t cv_debugtraversemax = CVAR_INIT ("debugtraversemax", TOSTR2(TRAVERSE_MAX), CV_NETVAR|CV_CHEAT, CV_Unsigned, NULL);
#endif

static CV_PossibleValue_t ingamecap_cons_t[] = {{0, "MIN"}, {MAXPLAYERS-1, "MAX"}, {0, NULL}};
consvar_t cv_ingamecap = CVAR_INIT ("ingamecap", "0", CV_NETVAR, ingamecap_cons_t, NULL);

static CV_PossibleValue_t spectatorreentry_cons_t[] = {{0, "MIN"}, {10*60, "MAX"}, {0, NULL}};
consvar_t cv_spectatorreentry = CVAR_INIT ("spectatorreentry", "0", CV_NETVAR, spectatorreentry_cons_t, NULL); // bugged in kart, default to 0 to match expected behavior

static CV_PossibleValue_t antigrief_cons_t[] = {{20, "MIN"}, {60, "MAX"}, {0, "Off"}, {0, NULL}};
consvar_t cv_antigrief = CVAR_INIT ("antigrief", "30", CV_NETVAR, antigrief_cons_t, NULL);

consvar_t cv_startinglives = CVAR_INIT ("startinglives", "3", CV_NETVAR|CV_CHEAT|CV_NOSHOWHELP, startingliveslimit_cons_t, NULL);

static CV_PossibleValue_t respawntime_cons_t[] = {{1, "MIN"}, {30, "MAX"}, {0, "Off"}, {0, NULL}};
consvar_t cv_respawntime = CVAR_INIT ("respawndelay", "1", CV_NETVAR|CV_CHEAT, respawntime_cons_t, NULL);

// names
consvar_t cv_playername[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("name", "Sonic", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name_OnChange),
	CVAR_INIT ("name2", "Tails", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name2_OnChange),
	CVAR_INIT ("name3", "Knuckles", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name3_OnChange),
	CVAR_INIT ("name4", "Eggman", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Name4_OnChange)
};
// player colors
UINT16 lastgoodcolor[MAXSPLITSCREENPLAYERS] = {SKINCOLOR_BLUE, SKINCOLOR_BLUE, SKINCOLOR_BLUE, SKINCOLOR_BLUE};
consvar_t cv_playercolor[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("color", "Blue", CV_SAVE|CV_CALL|CV_NOINIT, Color_cons_t, Color_OnChange),
	CVAR_INIT ("color2", "Orange", CV_SAVE|CV_CALL|CV_NOINIT, Color_cons_t, Color2_OnChange),
	CVAR_INIT ("color3", "Red", CV_SAVE|CV_CALL|CV_NOINIT, Color_cons_t, Color3_OnChange),
	CVAR_INIT ("color4", "Red", CV_SAVE|CV_CALL|CV_NOINIT, Color_cons_t, Color4_OnChange)
};
// player's skin, saved for commodity, when using a favorite skins wad..
consvar_t cv_skin[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("skin", DEFAULTSKIN, CV_SAVE|CV_CALL|CV_NOINIT, NULL, Skin_OnChange),
	CVAR_INIT ("skin2", DEFAULTSKIN2, CV_SAVE|CV_CALL|CV_NOINIT, NULL, Skin2_OnChange),
	CVAR_INIT ("skin3", DEFAULTSKIN3, CV_SAVE|CV_CALL|CV_NOINIT, NULL, Skin3_OnChange),
	CVAR_INIT ("skin4", DEFAULTSKIN4, CV_SAVE|CV_CALL|CV_NOINIT, NULL, Skin4_OnChange)
};

// player's followers. Also saved.
consvar_t cv_follower[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("follower", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Follower_OnChange),
	CVAR_INIT ("follower2", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Follower2_OnChange),
	CVAR_INIT ("follower3", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Follower3_OnChange),
	CVAR_INIT ("follower4", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Follower4_OnChange)
};

// player's follower colors... Also saved...
consvar_t cv_followercolor[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("followercolor", "Default", CV_SAVE|CV_CALL|CV_NOINIT, Followercolor_cons_t, Followercolor_OnChange),
	CVAR_INIT ("followercolor2", "Default", CV_SAVE|CV_CALL|CV_NOINIT, Followercolor_cons_t, Followercolor2_OnChange),
	CVAR_INIT ("followercolor3", "Default", CV_SAVE|CV_CALL|CV_NOINIT, Followercolor_cons_t, Followercolor3_OnChange),
	CVAR_INIT ("followercolor4", "Default", CV_SAVE|CV_CALL|CV_NOINIT, Followercolor_cons_t, Followercolor4_OnChange)
};

// player's voices...also, uh, saved.
consvar_t cv_voice[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("voice", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice_OnChange),
	CVAR_INIT ("voice2", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice2_OnChange),
	CVAR_INIT ("voice3", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice3_OnChange),
	CVAR_INIT ("voice4", "None", CV_SAVE|CV_CALL|CV_NOINIT, NULL, Voice4_OnChange)
};

static CV_PossibleValue_t restatvalue_cons_t[] = {{0, "MIN"}, {9, "MAX"}, {0, NULL}};
consvar_t cv_dummyrestatspeed[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("restatspeed", "0", 0, restatvalue_cons_t, NULL),
	CVAR_INIT ("restatspeed2", "0", 0, restatvalue_cons_t, NULL),
	CVAR_INIT ("restatspeed3", "0", 0, restatvalue_cons_t, NULL),
	CVAR_INIT ("restatspeed4", "0", 0, restatvalue_cons_t, NULL)
};
consvar_t cv_dummyrestatweight[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("restatweight", "0", 0, restatvalue_cons_t, NULL),
	CVAR_INIT ("restatweight2", "0", 0, restatvalue_cons_t, NULL),
	CVAR_INIT ("restatweight3", "0", 0, restatvalue_cons_t, NULL),
	CVAR_INIT ("restatweight4", "0", 0, restatvalue_cons_t, NULL)
};

consvar_t cv_dummyrestatrandom[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("restatrandom", "No", 0, CV_YesNo, NULL),
	CVAR_INIT ("restatrandom2", "No", 0, CV_YesNo, NULL),
	CVAR_INIT ("restatrandom3", "No", 0, CV_YesNo, NULL),
	CVAR_INIT ("restatrandom4", "No", 0, CV_YesNo, NULL)
};

consvar_t cv_skipmapcheck = CVAR_INIT ("skipmapcheck", "Off", CV_SAVE, CV_OnOff, NULL);

consvar_t cv_usemouse = CVAR_INIT ("use_mouse", "Off", CV_SAVE|CV_CALL,usemouse_cons_t, I_StartupMouse);

consvar_t cv_laglesscam = CVAR_INIT ("laglesscamera", "Off", CV_SAVE,CV_OnOff, NULL);

consvar_t cv_usecontroller[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("use_joystick", "1", CV_SAVE|CV_CALL, usejoystick_cons_t, I_InitController1),
	CVAR_INIT ("use_joystick2", "2", CV_SAVE|CV_CALL, usejoystick_cons_t, I_InitController2),
	CVAR_INIT ("use_joystick3", "3", CV_SAVE|CV_CALL, usejoystick_cons_t, I_InitController3),
	CVAR_INIT ("use_joystick4", "4", CV_SAVE|CV_CALL, usejoystick_cons_t, I_InitController4)
};

#if (defined (LJOYSTICK) || defined (HAVE_SDL))
consvar_t cv_controllerscale[MAXSPLITSCREENPLAYERS] = {
	CVAR_INIT ("padscale", "1", CV_SAVE|CV_CALL, NULL, I_ControllerScale),
	CVAR_INIT ("padscale2", "1", CV_SAVE|CV_CALL, NULL, I_ControllerScale2),
	CVAR_INIT ("padscale3", "1", CV_SAVE|CV_CALL, NULL, I_ControllerScale3),
	CVAR_INIT ("padscale4", "1", CV_SAVE|CV_CALL, NULL, I_ControllerScale4)
};
#else
consvar_t cv_controllerscale[MAXSPLITSCREENPLAYERS] = { //Alam: Dummy for save
	CVAR_INIT ("padscale", "1", CV_SAVE|CV_HIDEN, NULL, NULL),
	CVAR_INIT ("padscale2", "1", CV_SAVE|CV_HIDEN, NULL, NULL),
	CVAR_INIT ("padscale3", "1", CV_SAVE|CV_HIDEN, NULL, NULL),
	CVAR_INIT ("padscale4", "1", CV_SAVE|CV_HIDEN, NULL, NULL)
};
#endif

// SRB2kart
consvar_t cv_lastserver = CVAR_INIT ("lastserver", "", CV_SAVE, NULL, NULL);
static CV_PossibleValue_t kartminimap_cons_t[] = {{0, "MIN"}, {10, "MAX"}, {0, NULL}};
consvar_t cv_kartminimap = CVAR_INIT ("kartminimap", "4", CV_SAVE, kartminimap_cons_t, NULL);
consvar_t cv_kartcheck = CVAR_INIT ("kartcheck", "Yes", CV_SAVE, CV_YesNo, NULL);
static CV_PossibleValue_t kartinvinsfx_cons_t[] = {{0, "Music"}, {1, "SFX"}, {0, NULL}};
consvar_t cv_kartinvinsfx = CVAR_INIT ("kartinvinsfx", "SFX", CV_SAVE, kartinvinsfx_cons_t, NULL);
consvar_t cv_kartspeed = CVAR_INIT ("kartspeed", "Auto", CV_NETVAR|CV_CALL|CV_NOINIT, kartspeed_cons_t, KartSpeed_OnChange);
consvar_t cv_kartbattlespeed = CVAR_INIT ("kartbattlespeed", "Auto", CV_NETVAR|CV_CALL|CV_NOINIT, kartspeed_cons_t, KartBattleSpeed_OnChange);
static CV_PossibleValue_t kartbumpers_cons_t[] = {{1, "MIN"}, {12, "MAX"}, {0, NULL}};
consvar_t cv_kartbumpers = CVAR_INIT ("kartbumpers", "3", CV_NETVAR|CV_CHEAT, kartbumpers_cons_t, NULL);
consvar_t cv_kartfrantic = CVAR_INIT ("kartfrantic", "Off", CV_NETVAR|CV_CHEAT|CV_CALL|CV_NOINIT, CV_OnOff, KartFrantic_OnChange);
consvar_t cv_kartcomeback = CVAR_INIT ("kartcomeback", "On", CV_NETVAR|CV_CHEAT|CV_CALL|CV_NOINIT, CV_OnOff, KartComeback_OnChange);
static CV_PossibleValue_t kartencore_cons_t[] = {{-1, "Auto"}, {0, "Off"}, {1, "On"}, {0, NULL}};
consvar_t cv_kartencore = CVAR_INIT ("kartencore", "Auto", CV_NETVAR|CV_CALL|CV_NOINIT, kartencore_cons_t, KartEncore_OnChange);
static CV_PossibleValue_t kartvoterulechanges_cons_t[] = {{0, "Never"}, {1, "Sometimes"}, {2, "Frequent"}, {3, "Always"}, {0, NULL}};
consvar_t cv_kartvoterulechanges = CVAR_INIT ("kartvoterulechanges", "Frequent", CV_NETVAR, kartvoterulechanges_cons_t, NULL);
static CV_PossibleValue_t kartgametypepreference_cons_t[] = {{-1, "None"}, {GT_RACE, "Race"}, {GT_BATTLE, "Battle"}, {0, NULL}};
consvar_t cv_kartgametypepreference = CVAR_INIT ("kartgametypepreference", "None", CV_NETVAR, kartgametypepreference_cons_t, NULL);
static CV_PossibleValue_t kartspeedometer_cons_t[] = {{0, "Off"}, {1, "Kilometers"}, {2, "Miles"}, {3, "Fracunits"}, {4, "Percentage"}, {0, NULL}};
consvar_t cv_kartspeedometer = CVAR_INIT ("kartdisplayspeed", "Percentage", CV_SAVE, kartspeedometer_cons_t, NULL); // use tics in display
static CV_PossibleValue_t kartvoices_cons_t[] = {{0, "Never"}, {1, "Tasteful"}, {2, "Meme"}, {0, NULL}};
consvar_t cv_kartvoices = CVAR_INIT ("kartvoices", "Tasteful", CV_SAVE, kartvoices_cons_t, NULL);
consvar_t cv_karthorns = CVAR_INIT ("taunthorns", "Tasteful", CV_SAVE, kartvoices_cons_t, NULL);

static CV_PossibleValue_t kartbot_cons_t[] = {
	{0, "Off"},
	{1, "Lv.1"},
	{2, "Lv.2"},
	{3, "Lv.3"},
	{4, "Lv.4"},
	{5, "Lv.5"},
	{6, "Lv.6"},
	{7, "Lv.7"},
	{8, "Lv.8"},
	{9, "Lv.9"},
	{10,"Lv.10"},
	{11,"Lv.11"},
	{12,"Lv.12"},
	{13,"Lv.MAX"},
	{0, NULL}
};
static CV_PossibleValue_t kartbot_cap_cons_t[] = {{0, "MIN"}, {MAXPLAYERS, "MAX"}, {0, NULL}};
consvar_t cv_kartbot = CVAR_INIT ("kartbot", "0", CV_NETVAR, kartbot_cons_t, NULL);
consvar_t cv_kartbot_cap = CVAR_INIT ("kartbot_cap", "0", CV_NETVAR, kartbot_cap_cons_t, NULL);
consvar_t cv_kartbot_modifiermax = CVAR_INIT ("kartbot_modifiermax", "1.0", CV_NETVAR|CV_FLOAT, CV_Unsigned, NULL);
consvar_t cv_kartbot_basetrackcomplexity = CVAR_INIT ("kartbot_basetrackcomplexity", "-7000", CV_NETVAR, CV_Signed, NULL);

consvar_t cv_karteliminatelast = CVAR_INIT ("karteliminatelast", "Yes", CV_NETVAR|CV_CALL, CV_YesNo, KartEliminateLast_OnChange);

// Toggles for new features
consvar_t cv_kartrings = CVAR_INIT ("kartrings", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartRings_OnChange);

static CV_PossibleValue_t ringsmin_cons_t[] = {{INT8_MIN, "MIN"}, {0, "MAX"}, {0, NULL}};
static CV_PossibleValue_t ringsmax_cons_t[] = {{0, "MIN"}, {INT8_MAX, "MAX"}, {0, NULL}};

consvar_t cv_kartringsmin = CVAR_INIT ("kartringsmin", "-20", CV_NETVAR, ringsmin_cons_t, NULL);
consvar_t cv_kartringsmax = CVAR_INIT ("kartringsmax", "20", CV_NETVAR, ringsmax_cons_t, NULL);

static CV_PossibleValue_t ringsstart_cons_t[] = {{INT8_MIN, "MIN"}, {INT8_MAX, "MAX"}, {0, NULL}};
consvar_t cv_kartringsstart = CVAR_INIT ("kartringsstart", "5", CV_NETVAR, ringsstart_cons_t, NULL);

// Stacking related
consvar_t cv_kartstacking = CVAR_INIT ("kartstacking", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartStacking_OnChange);
consvar_t cv_kartstacking_diminishparam = CVAR_INIT ("kartstacking_diminishparam", "0.32", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_maxvanillaboost = CVAR_INIT ("kartstacking_maxvanillaboost", "0.375", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_speedboostdropoff = CVAR_INIT ("kartstacking_speedboostdropoff", "0.01", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_speedboostdropoff_brake = CVAR_INIT ("kartstacking_speedboostdropoff_brake", "0.05", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
// V recreates neptunes old ass misread of stacking code lmao
consvar_t cv_kartstacking_accelstack = CVAR_INIT ("kartstacking_accelstack", "Off", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

// Vanilla Stacking boosts.
consvar_t cv_kartstacking_sneaker_easyspeedboost = CVAR_INIT ("vanillaboost_sneaker_easyspeedboost", "0.8317", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_sneaker_normalspeedboost = CVAR_INIT ("vanillaboost_sneaker_normalspeedboost", "0.5", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_sneaker_hardspeedboost = CVAR_INIT ("vanillaboost_sneaker_hardspeedboost", "0.2756", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_sneaker_expertspeedboost = CVAR_INIT ("vanillaboost_sneaker_expertspeedboost", "0.25", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_sneaker_accelboost = CVAR_INIT ("vanillaboost_sneaker_accelboost", "8.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_sneaker_handleboost = CVAR_INIT ("vanillaboost_sneaker_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_sneaker_maxgrade = CVAR_INIT ("vanillaboost_sneaker_maxgrade", "3", CV_NETVAR|CV_CHEAT|CV_GUARD, CV_Natural, NULL);
consvar_t cv_kartstacking_sneaker_stackable = CVAR_INIT ("vanillaboost_sneaker_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);
consvar_t cv_kartstacking_panel_separate = CVAR_INIT ("vanillaboost_panel_separate", "Off", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);
consvar_t cv_kartstacking_panel_maxgrade = CVAR_INIT ("vanillaboost_panel_maxgrade", "2", CV_NETVAR|CV_CHEAT|CV_GUARD, CV_Natural, NULL);

//
// Invincibility
//
consvar_t cv_kartstacking_invincibility_speedboost = CVAR_INIT ("vanillaboost_invincibility_speedboost", "0.375", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_invincibility_accelboost = CVAR_INIT ("vanillaboost_invincibility_accelboost", "3.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_invincibility_handleboost = CVAR_INIT ("vanillaboost_invincibility_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_invincibility_stackable = CVAR_INIT ("vanillaboost_invincibility_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

//
// S-Monitor
//
consvar_t cv_kartstacking_smonitor_speedboost = CVAR_INIT ("vanillaboost_smonitor_speedboost", "0.75", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_smonitor_accelboost = CVAR_INIT ("vanillaboost_smonitor_accelboost", "3.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_smonitor_handleboost = CVAR_INIT ("vanillaboost_smonitor_handleboost", "0.48", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_smonitor_stackable = CVAR_INIT ("vanillaboost_smonitor_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_grow_speedboost = CVAR_INIT ("vanillaboost_grow_speedboost", "0.2", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_grow_accelboost = CVAR_INIT ("vanillaboost_grow_accelboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_grow_handleboost = CVAR_INIT ("vanillaboost_grow_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_grow_stackable = CVAR_INIT ("vanillaboost_grow_stackable", "Off", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_altshrink_speedboost = CVAR_INIT ("vanillaboost_altshrink_speedboost", "0.50", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_altshrink_accelboost = CVAR_INIT ("vanillaboost_altshrink_accelboost", "0.15", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_altshrink_handleboost = CVAR_INIT ("vanillaboost_altshrink_handleboost", "0.45", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_altshrink_stackable = CVAR_INIT ("vanillaboost_altshrink_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_bubble_speedboost = CVAR_INIT ("vanillaboost_bubble_speedboost", "0.35", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_bubble_accelboost = CVAR_INIT ("vanillaboost_bubble_accelboost", "8.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_bubble_handleboost = CVAR_INIT ("vanillaboost_bubble_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_bubble_stackable = CVAR_INIT ("vanillaboost_bubble_stackable", "Off", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_flame_speedval = CVAR_INIT ("vanillaboost_flame_speedval", "0.20", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_flame_accelboost = CVAR_INIT ("vanillaboost_flame_accelboost", "8.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_flame_handleboost = CVAR_INIT ("vanillaboost_flame_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_flame_stackable = CVAR_INIT ("vanillaboost_flame_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_start_speedboost = CVAR_INIT ("vanillaboost_start_speedboost", "0.25", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_start_accelboost = CVAR_INIT ("vanillaboost_start_accelboost", "6.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_start_handleboost = CVAR_INIT ("vanillaboost_start_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_start_stackable = CVAR_INIT ("vanillaboost_start_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_walltransfer_speedboost = CVAR_INIT ("vanillaboost_walltransfer_speedboost", "0.10", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_walltransfer_accelboost = CVAR_INIT ("vanillaboost_walltransfer_accelboost", "5.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_walltransfer_handleboost = CVAR_INIT ("vanillaboost_walltransfer_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_walltransfer_stackable = CVAR_INIT ("vanillaboost_walltransfer_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_drift_speedboost = CVAR_INIT ("vanillaboost_drift_speedboost", "0.25", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_drift_accelboost = CVAR_INIT ("vanillaboost_drift_accelboost", "4.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_drift_handleboost = CVAR_INIT ("vanillaboost_drift_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_drift_stackable = CVAR_INIT ("vanillaboost_drift_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_ring_speedboost = CVAR_INIT ("vanillaboost_ring_speedboost", "0.2", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_ring_accelboost = CVAR_INIT ("vanillaboost_ring_accelboost", "4.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_ring_handleboost = CVAR_INIT ("vanillaboost_ring_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_ring_stackable = CVAR_INIT ("vanillaboost_ring_stackable", "On", CV_NETVAR|CV_CHEAT|CV_GUARD, CV_OnOff, NULL);

consvar_t cv_kartstacking_heavydrop_speedboost = CVAR_INIT ("vanillaboost_heavydrop_speedboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_heavydrop_accelboost = CVAR_INIT ("vanillaboost_heavydrop_accelboost", "8.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_heavydrop_handleboost = CVAR_INIT ("vanillaboost_heavydrop_handleboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_heavydrop_stackable = CVAR_INIT ("vanillaboost_heavydrop_stackable", "On", CV_NETVAR|CV_CHEAT|CV_GUARD, CV_OnOff, NULL);
consvar_t cv_kartstacking_heavydrop_uniform = CVAR_INIT ("vanillaboost_heavydrop_uniform", "No", CV_NETVAR|CV_CHEAT|CV_GUARD, CV_YesNo, NULL);

consvar_t cv_kartstacking_ssmt_speedboost = CVAR_INIT ("vanillaboost_ssmt_speedboost", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_ssmt_accelboost = CVAR_INIT ("vanillaboost_ssmt_accelboost", "10.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_ssmt_handleboost = CVAR_INIT ("vanillaboost_ssmt_handleboost", "0.01", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);

consvar_t cv_kartstacking_recspin_speedboost_lo = CVAR_INIT ("vanillaboost_recspin_speedboost_lo", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_recspin_accelboost_lo = CVAR_INIT ("vanillaboost_recspin_accelboost_lo", "0.5", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_recspin_handleboost_lo = CVAR_INIT ("vanillaboost_recspin_handleboost_lo", "0.25", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);

consvar_t cv_kartstacking_recspin_speedboost_hi = CVAR_INIT ("vanillaboost_recspin_speedboost_hi", "0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_recspin_accelboost_hi = CVAR_INIT ("vanillaboost_recspin_accelboost_hi", "8.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_recspin_handleboost_hi = CVAR_INIT ("vanillaboost_recspin_handleboost_hi", "0.5", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);

consvar_t cv_kartstacking_slope_decay = CVAR_INIT ("vanillaboost_slope_decay", "0.004", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_slope_brakemod = CVAR_INIT ("vanillaboost_slope_brakemod", "0.01", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_slope_speedboost_max = CVAR_INIT ("vanillaboost_slope_speedboost_max", "3.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_slope_speedboost_cap = CVAR_INIT ("vanillaboost_slope_speedboost_cap", "1.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_slope_accelboost = CVAR_INIT ("vanillaboost_slope_accelboost", "3.0", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartstacking_slope_stackable = CVAR_INIT ("vanillaboost_slope_stackable", "On", CV_NETVAR|CV_GUARD, CV_OnOff, NULL);

// Max speed for high speeds
consvar_t cv_kartstacking_drafting_minspeed = CVAR_INIT ("vanillaboost_draft_minspeed", "0.16", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);

// Max speed for low speeds
consvar_t cv_kartstacking_drafting_maxspeed = CVAR_INIT ("vanillaboost_draft_maxspeed", "0.26", CV_NETVAR|CV_CHEAT|CV_FLOAT|CV_GUARD, CV_Unsigned, NULL);

consvar_t cv_kartchaining = CVAR_INIT ("kartchaining", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartChaining_OnChange);
consvar_t cv_kartchainingoffroad = CVAR_INIT ("kartchaining_chainoffroad", "No", CV_NETVAR, CV_YesNo, NULL);

consvar_t cv_kartitembreaker = CVAR_INIT ("kartitembreaker", "No", CV_NETVAR|CV_CALL|CV_NOINIT, CV_YesNo, KartItemBreaker_OnChange);

//consvar_t cv_kartwalltransfer = CVAR_INIT ("BG_forcewalltransfer", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);

consvar_t cv_kartusepwrlv = CVAR_INIT ("kartusepwrlv", "Yes", CV_NETVAR, CV_YesNo, NULL);

consvar_t cv_kartpurpledrift = CVAR_INIT ("kartpurpledrift", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartPurpleDrift_OnChange);

static CV_PossibleValue_t bumpspark_cons_t[] = {{BUMPSPARK_NONE, "Off"},
						{BUMPSPARK_NOCHARGE, "Remove Charge Only"},
						{BUMPSPARK_RESET100, "Reset to Blue"},
						{BUMPSPARK_ALL, "On"},
						{0, NULL}};

consvar_t cv_kartbumpspark = CVAR_INIT("kartbumpspark",
				       "Remove Charge Only",
				       CV_NETVAR | CV_CALL | CV_NOINIT | CV_GUARD,
				       bumpspark_cons_t,
				       KartBumpSpark_OnChange);

consvar_t cv_kartbumpspring = CVAR_INIT ("kartbumpspring", "Yes", CV_NETVAR, CV_YesNo, NULL);

consvar_t cv_kartexplosion_limitlifetime = CVAR_INIT ("kartexplosion_limitlifetime", "Off", CV_NETVAR, CV_OnOff, NULL);
consvar_t cv_kartexplosion_limitlifetime_cap = CVAR_INIT ("kartexplosion_limitlifetime_cap", "12", CV_NETVAR, CV_Natural, NULL);

consvar_t cv_kartslipdash = CVAR_INIT ("kartslipdash", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartSlipdash_OnChange);

consvar_t cv_kartslopeboost = CVAR_INIT ("kartslopeboost", "No", CV_NETVAR|CV_CALL|CV_NOINIT, CV_YesNo, KartSlopeBoost_OnChange);

consvar_t cv_kartdrafting = CVAR_INIT ("kartdrafting", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartDrafting_OnChange);
consvar_t cv_kartdrafting_closedraft = CVAR_INIT ("kartdrafting_closedraft", "Off", CV_NETVAR, CV_OnOff, NULL);
consvar_t cv_kartdrafting_closedeadzone = CVAR_INIT ("kartdrafting_closedeadzone", "640", CV_NETVAR|CV_CHEAT, CV_Unsigned, NULL);
consvar_t cv_kartdrafting_basedistance = CVAR_INIT ("kartdrafting_basedistance", "2560", CV_NETVAR|CV_CHEAT, CV_Unsigned, NULL);

static CV_PossibleValue_t airdrop_cons_t[] = {{AIRDROP_NONE, "Off"},
						{AIRDROP_LIGHT, "Light"},
						{AIRDROP_HEAVY, "Heavy"},
						{AIRDROP_FUSION, "Fusion"},
						{0, NULL}};
consvar_t cv_kartairdrop = CVAR_INIT ("kartairdrop", "Off", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, airdrop_cons_t, KartAirDrop_OnChange);

consvar_t cv_kartairthrust = CVAR_INIT ("kartairthrust", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartAirThrust_OnChange);
consvar_t cv_kartairthrust_reductionrate = CVAR_INIT ("kartairthrust_reductionrate", "0.5", CV_NETVAR|CV_FLOAT|CV_CHEAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartairthrust_power1 = CVAR_INIT ("kartairthrust_power1", "0.2", CV_NETVAR|CV_FLOAT|CV_CHEAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartairthrust_power2 = CVAR_INIT ("kartairthrust_power2", "0.35", CV_NETVAR|CV_FLOAT|CV_CHEAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartairthrust_power3 = CVAR_INIT ("kartairthrust_power3", "0.5", CV_NETVAR|CV_FLOAT|CV_CHEAT|CV_GUARD, CV_Unsigned, NULL);
consvar_t cv_kartairthrust_power4 = CVAR_INIT ("kartairthrust_power4", "0.75", CV_NETVAR|CV_FLOAT|CV_CHEAT|CV_GUARD, CV_Unsigned, NULL);

consvar_t cv_kartwaterskiplock = CVAR_INIT ("kartwaterskiplock", "Yes", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartWaterSkipLock_OnChange);

consvar_t cv_kartitemlitter = CVAR_INIT ("kartitemlitter", "On", CV_NETVAR|CV_CALL|CV_NOINIT, CV_OnOff, KartItemLitter_OnChange);
consvar_t cv_kartitempush = CVAR_INIT ("kartitempush", "Off", CV_NETVAR|CV_CALL|CV_NOINIT, CV_OnOff, KartItemPush_OnChange);

consvar_t cv_kartforcelegacyodds = CVAR_INIT ("kartforcelegacyodds", "Off", CV_NETVAR, CV_OnOff, NULL);

consvar_t cv_handleboostslip = CVAR_INIT ("karthandleboostsliptide", "Off", CV_NETVAR, CV_OnOff, NULL);

consvar_t cv_kartrecoverydash = CVAR_INIT ("kartrecoverydash", "No", CV_NETVAR|CV_CALL|CV_NOINIT|CV_GUARD, CV_YesNo, KartRecoveryDash_OnChange);
consvar_t cv_kartrecoverydash_spinspeed = CVAR_INIT ("kartrecoverydash_spinspeed", "24.0", CV_NETVAR|CV_FLOAT|CV_CHEAT|CV_GUARD, CV_Unsigned, NULL);

#define ANTIBUMP_MAX (UINT32_MAX / TICRATE)
static CV_PossibleValue_t antibump_cons_t[] = {{0, "MIN"}, {ANTIBUMP_MAX, "MAX"}, {0, NULL}};
consvar_t cv_kartantibump = CVAR_INIT ("kartantibump", "0", CV_NETVAR|CV_CALL|CV_NOINIT, antibump_cons_t, KartAntiBump_OnChange);
#undef ANTIBUMP_MAX

// Odds distancing
#define MAXODDSDIST ((INT32_MAX / FRACUNIT) / 2)
static CV_PossibleValue_t distvar_cons_t[] = {{1, "MIN"}, {MAXODDSDIST, "MAX"}, {0, NULL}};
consvar_t cv_kartoddsdist = CVAR_INIT ("kartoddsdist", "1675", CV_NETVAR|CV_CHEAT|CV_GUARD, distvar_cons_t, NULL);
consvar_t cv_kartlegacyoddsdist = CVAR_INIT ("kartlegacyoddsdist", "1500", CV_NETVAR|CV_CHEAT|CV_GUARD, distvar_cons_t, NULL);
#undef MAXODDSDIST

// SPB Rush toggle; toggles on/off the more aggressive "pressure" odds when an SPB is active
consvar_t cv_kartspbrush = CVAR_INIT ("kartspbrush", "On", CV_NETVAR, CV_OnOff, NULL);

consvar_t cv_itemlist = CVAR_INIT ("kartitemlist", "On", CV_NETVAR|CV_CALL|CV_NOINIT, CV_OnOff, KartItemList_OnChange);

// Time limit for Alt. Shrink
static CV_PossibleValue_t altshrinktime_cons_t[] = {{0, "MIN"}, {(INT16_MAX / TICRATE), "MAX"}, {0, NULL}};
consvar_t cv_kartaltshrinktime = CVAR_INIT ("kartaltshrinktime", "14", CV_NETVAR|CV_CHEAT|CV_GUARD, altshrinktime_cons_t, NULL);

// Toggle Full or Standard Invincibility music
static CV_PossibleValue_t kartinvintheme_cons_t[] = {{0, "Standard"}, {1, "Full"}, {0, NULL}};
consvar_t cv_kartinvintheme = CVAR_INIT ("kartinvintheme", "Standard", CV_SAVE, kartinvintheme_cons_t, NULL);

// Invincibility damage toggles
static CV_PossibleValue_t kartinvindmg_cons_t[] = {{0, "None"}, {DMG_WIPEOUT, "Wipe-out"}, {DMG_FLIPOVER, "Flip-over"}, {0, NULL}};

// Experiment: Let's allow changing the damage type
consvar_t cv_kartinvindamage = CVAR_INIT ("kartinvindamage", "Wipe-out", CV_NETVAR|CV_CHEAT|CV_GUARD, kartinvindmg_cons_t, NULL);

// How far the player must be from the cluster to roll an S-Monitor.
static CV_PossibleValue_t smonitordist_cons_t[] = {{1, "MIN"}, {32000, "MAX"}, {0, NULL}};
consvar_t cv_kartsmonitordist = CVAR_INIT ("smonitor_startdist", "12000", CV_NETVAR|CV_CHEAT|CV_GUARD, smonitordist_cons_t, NULL);
consvar_t cv_kartsmonitor_decaydist = CVAR_INIT ("smonitor_decaydist", "3600", CV_NETVAR|CV_CHEAT|CV_GUARD, smonitordist_cons_t, NULL);

static CV_PossibleValue_t smonitordecaytime_cons_t[] = {{1, "MIN"}, {6, "MAX"}, {0, NULL}};
consvar_t cv_kartsmonitor_decaytime = CVAR_INIT ("smonitor_decaytime", "2", CV_NETVAR|CV_CHEAT|CV_GUARD, smonitordecaytime_cons_t, NULL);

// opinionated stuff for testing balance tweaks on the shields
consvar_t cv_kartbubble_defense_canidle = CVAR_INIT ("kartbubble_defense_canidle", "On", CV_NETVAR, CV_OnOff, NULL);
static CV_PossibleValue_t bubble_defense_damagerate_cons_t[] = {{0, "MIN"}, {FRACUNIT, "MAX"}, {0, NULL}};
consvar_t cv_kartbubble_defense_damagerate = CVAR_INIT ("kartbubble_defense_damagerate", "1.0", CV_NETVAR|CV_FLOAT, bubble_defense_damagerate_cons_t, NULL);
consvar_t cv_kartbubble_boost_allow = CVAR_INIT ("kartbubble_boost_allow", "On", CV_NETVAR, CV_OnOff, NULL);
consvar_t cv_kartbubble_boost_offroadignore = CVAR_INIT ("kartbubble_boost_offroadignore", "On", CV_NETVAR, CV_OnOff, NULL);
consvar_t cv_kartflame_fastfuel = CVAR_INIT ("kartflame_fastfuel", "On", CV_NETVAR, CV_OnOff, NULL);
consvar_t cv_kartflame_offroadburn = CVAR_INIT ("kartflame_offroadburn", "On", CV_NETVAR, CV_OnOff, NULL);

// "Arrow Bullet": above a certain speed threshold, an aura in the shape of the Shrink arrow
// covers you, signaling protection from (most) items and players.
// Default threshold percentage is currently 166%.
consvar_t cv_kartaltshrink_arrowbullet = CVAR_INIT ("kart_altshrink_arrowbullet", "On", CV_NETVAR, CV_OnOff, NULL);

static CV_PossibleValue_t altshrink_arrowbullet_threshold_cons_t[] = {{0, "MIN"}, {2 * FRACUNIT, "MAX"}, {0, NULL}};
consvar_t cv_kartaltshrink_arrowbulletthres = CVAR_INIT ("kart_altshrink_arrowbullet_threshold", "1.45", CV_NETVAR|CV_FLOAT, altshrink_arrowbullet_threshold_cons_t, NULL);

static CV_PossibleValue_t kartairsquish_cons_t[] = {{1, "Squish"}, {2, "Flip-over"}, {0, "None"}, {0, NULL}};
consvar_t cv_kartairsquish = CVAR_INIT ("kartairsquish", "None", CV_NETVAR, kartairsquish_cons_t, NULL);

consvar_t cv_kartdebugshrink = CVAR_INIT ("kartdebugshrink", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);

consvar_t cv_kartdebugdistribution = CVAR_INIT ("kartdebugdistribution", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);
consvar_t cv_kartdebughuddrop = CVAR_INIT ("kartdebughuddrop", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);
static CV_PossibleValue_t kartdebugwaypoint_cons_t[] = {{0, "Off"}, {1, "Forwards"}, {2, "Backwards"}, {0, NULL}};
consvar_t cv_kartdebugwaypoints = CVAR_INIT ("kartdebugwaypoints", "Off", CV_NETVAR|CV_CHEAT, kartdebugwaypoint_cons_t, NULL);
consvar_t cv_kartdebuglap = CVAR_INIT ("kartdebuglap", "Off", CV_NETVAR|CV_CHEAT, kartdebugwaypoint_cons_t, NULL);
consvar_t cv_kartdebugbot = CVAR_INIT ("kartdebugbot", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);
consvar_t cv_kartdebugcluster = CVAR_INIT ("kartdebugcluster", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);
consvar_t cv_kartdebugrings = CVAR_INIT ("kartdebugrings", "Off", CV_NETVAR|CV_CHEAT, CV_OnOff, NULL);

consvar_t cv_kartdebugcheckpoint = CVAR_INIT ("kartdebugcheckpoint", "Off", 0, CV_OnOff, NULL);
consvar_t cv_kartdebugnodes = CVAR_INIT ("kartdebugnodes", "Off", 0, CV_OnOff, NULL);
consvar_t cv_kartdebugcolorize = CVAR_INIT ("kartdebugcolorize", "Off", 0, CV_OnOff, NULL);
consvar_t cv_kartdebugdirector = CVAR_INIT ("kartdebugdirector", "Off", 0, CV_OnOff, NULL);
consvar_t cv_gptest = CVAR_INIT ("gptest", "Off", CV_CHEAT|CV_NETVAR, CV_OnOff, NULL);

static CV_PossibleValue_t votetime_cons_t[] = {{10, "MIN"}, {3600, "MAX"}, {0, NULL}};
consvar_t cv_votetime = CVAR_INIT ("votetime", "20", CV_NETVAR, votetime_cons_t, NULL);

consvar_t cv_gravity = CVAR_INIT ("gravity", "0.8", CV_RESTRICT|CV_FLOAT|CV_CALL, NULL, Gravity_OnChange); // change DEFAULT_GRAVITY if you change this

static CV_PossibleValue_t soundtest_cons_t[] = {{0, "MIN"}, {NUMSFX, "MAX"}, {0, NULL}};
consvar_t cv_soundtest = CVAR_INIT ("soundtest", "0", CV_CALL, soundtest_cons_t, SoundTest_OnChange);

static CV_PossibleValue_t minitimelimit_cons_t[] = {{15, "MIN"}, {9999, "MAX"}, {0, NULL}};
consvar_t cv_countdowntime = CVAR_INIT ("countdowntime", "30", CV_NETVAR|CV_CHEAT, minitimelimit_cons_t, NULL);

consvar_t cv_autobalance = CVAR_INIT ("autobalance", "Off", CV_SAVE|CV_NETVAR|CV_CALL, CV_OnOff, AutoBalance_OnChange);
consvar_t cv_teamscramble = CVAR_INIT ("teamscramble", "Off", CV_SAVE|CV_NETVAR|CV_CALL|CV_NOINIT, teamscramble_cons_t, TeamScramble_OnChange);
consvar_t cv_scrambleonchange = CVAR_INIT ("scrambleonchange", "Off", CV_SAVE|CV_NETVAR, teamscramble_cons_t, NULL);

consvar_t cv_friendlyfire = CVAR_INIT ("friendlyfire", "Off", CV_NETVAR|CV_NOSHOWHELP, CV_OnOff, NULL);
consvar_t cv_itemfinder = CVAR_INIT ("itemfinder", "Off", CV_CALL|CV_NOSHOWHELP, CV_OnOff, ItemFinder_OnChange);

// Scoring type options
consvar_t cv_overtime = CVAR_INIT ("overtime", "Yes", CV_NETVAR|CV_CHEAT, CV_YesNo, NULL);

consvar_t cv_rollingdemos = CVAR_INIT ("rollingdemos", "Off", CV_SAVE, CV_OnOff, NULL);

static CV_PossibleValue_t pointlimit_cons_t[] = {{1, "MIN"}, {MAXSCORE, "MAX"}, {0, "None"}, {0, NULL}};
consvar_t cv_pointlimit = CVAR_INIT ("pointlimit", "None", CV_NETVAR|CV_CALL|CV_NOINIT, pointlimit_cons_t, PointLimit_OnChange);
static CV_PossibleValue_t timelimit_cons_t[] = {{1, "MIN"}, {99999, "MAX"}, {0, "None"}, {0, NULL}};
consvar_t cv_timelimit = CVAR_INIT ("timelimit", "2", CV_NETVAR|CV_CALL|CV_NOINIT, timelimit_cons_t, TimeLimit_OnChange);

static CV_PossibleValue_t numlaps_cons_t[] = {{0, "MIN"}, {MAX_LAPS, "MAX"}, {-1, "Map default"}, {0, NULL}};
consvar_t cv_numlaps = CVAR_INIT ("numlaps", "Map default", CV_NETVAR|CV_CALL|CV_CHEAT, numlaps_cons_t, NumLaps_OnChange);

consvar_t cv_forceskin = CVAR_INIT ("forceskin", "None", CV_NETVAR|CV_CALL|CV_CHEAT, NULL, ForceSkin_OnChange);

consvar_t cv_downloading = CVAR_INIT ("downloading", "On", 0, CV_OnOff, NULL);
consvar_t cv_allowexitlevel = CVAR_INIT ("allowexitlevel", "No", CV_NETVAR, CV_YesNo, NULL);

static CV_PossibleValue_t votemaxrows_cons_t[] = {{1, "MIN"}, {3, "MAX"}, {0, NULL}};
consvar_t cv_votemaxrows = CVAR_INIT ("votemaxrows", "2", CV_SAVE|CV_NETVAR, votemaxrows_cons_t, NULL);

consvar_t cv_netstat = CVAR_INIT ("netstat", "Off", 0, CV_OnOff, NULL); // show bandwidth statistics
static CV_PossibleValue_t nettimeout_cons_t[] = {{TICRATE/7, "MIN"}, {60*TICRATE, "MAX"}, {0, NULL}};
consvar_t cv_nettimeout = CVAR_INIT ("nettimeout", "210", CV_CALL|CV_SAVE, nettimeout_cons_t, NetTimeout_OnChange);
//static CV_PossibleValue_t jointimeout_cons_t[] = {{5*TICRATE, "MIN"}, {60*TICRATE, "MAX"}, {0, NULL}};
consvar_t cv_jointimeout = CVAR_INIT ("jointimeout", "210", CV_CALL|CV_SAVE, nettimeout_cons_t, JoinTimeout_OnChange);
consvar_t cv_maxping = CVAR_INIT ("maxdelay", "20", CV_SAVE, CV_Unsigned, NULL);

static CV_PossibleValue_t lagless_cons_t[] = {{-1, "Worst"}, {0, "Best"}, {1, "Lagless"}, {0, NULL}};
consvar_t cv_lagless = CVAR_INIT ("lagless", "Lagless", CV_SAVE|CV_NETVAR|CV_CALL, lagless_cons_t, Lagless_OnChange);

static CV_PossibleValue_t pingtimeout_cons_t[] = {{8, "MIN"}, {120, "MAX"}, {0, NULL}};
consvar_t cv_pingtimeout = CVAR_INIT ("maxdelaytimeout", "10", CV_SAVE, pingtimeout_cons_t, NULL);

// show your ping on the HUD next to framerate. Defaults to warning only (shows up if your ping is > maxping)
static CV_PossibleValue_t showping_cons_t[] = {{0, "Off"}, {1, "Always"}, {2, "Warning"}, {0, NULL}};
consvar_t cv_showping = CVAR_INIT ("showping", "Always", CV_SAVE, showping_cons_t, NULL);

static CV_PossibleValue_t pingmeasurement_cons_t[] = {{0, "Frames"}, {1, "Milliseconds"}, {0, NULL}};
consvar_t cv_pingmeasurement = CVAR_INIT ("pingmeasurement", "Frames", CV_SAVE, pingmeasurement_cons_t, NULL);
consvar_t cv_pingicon = CVAR_INIT ("pingicon", "On", CV_SAVE, CV_OnOff, NULL);

static CV_PossibleValue_t cv_pingstyle_cons_t[] = {{0, "New"}, {1, "Old"}, {0, NULL}};
consvar_t cv_pingstyle = CVAR_INIT ("pingstyle", "New", CV_SAVE, cv_pingstyle_cons_t, NULL);

static CV_PossibleValue_t showlapemblem_cons_t[] = {{0, "Off"}, {1, "Emblem Only"}, {2, "Splits Only"}, {3, "All"}, {0, NULL}};
consvar_t cv_showlapemblem = CVAR_INIT ("showlapemblem", "All", CV_SAVE, showlapemblem_cons_t, NULL);

static CV_PossibleValue_t lapemblemmode_cons_t[] = {{0, "Centre"}, {1, "Race Timer"}, {0, NULL}};
consvar_t cv_lapemblemmode = CVAR_INIT ("lapemblemmode", "Centre", CV_SAVE, lapemblemmode_cons_t, NULL);

// Race splits rely on showlapemblem, so toggling them off isn't really necessary.
static CV_PossibleValue_t racesplits_cons_t[] = {{1, "Next"}, {2, "Leader"}, {0, NULL}};
consvar_t cv_racesplits = CVAR_INIT ("racesplits", "Leader", CV_SAVE, racesplits_cons_t, NULL);

consvar_t cv_showminimapnames = CVAR_INIT ("showminimapnames", "Off", CV_SAVE, CV_OnOff, NULL);
static CV_PossibleValue_t minimapdot_cons_t[] = {{0, "Off"}, {1, "Dot"}, {2, "Headlight"}, {0, NULL}};
consvar_t cv_showminimapangle = CVAR_INIT ("showminimapangle", "Dot", CV_SAVE, minimapdot_cons_t, NULL);
consvar_t cv_minihead = CVAR_INIT ("smallminimapplayers", "Off", CV_SAVE, CV_OnOff, NULL);

static CV_PossibleValue_t spinoutroll_cons_t[] = {{0, "Off"}, {1, "Minimap Only"}, {2, "Rankings Only"}, {3, "Minimap+Rankings"}, {0, NULL}};
consvar_t cv_spinoutroll = CVAR_INIT ("spinoutroll", "Minimap+Rankings", CV_SAVE, spinoutroll_cons_t, NULL);

// Item/Ringbox Roulette drawn on player, from RadioRacers
consvar_t cv_rouletteonplayer = CVAR_INIT ("rouletteonplayer", "Off", CV_SAVE, CV_OnOff, NULL);

static CV_PossibleValue_t rlplrtrans_cons_t[] = {{0, "MIN"}, {10, "MAX"}, {0, NULL}};

// Controllable visibility for the player-tracking roulette icons.
// Default is 7 (70% visible; V_30TRANS)
consvar_t cv_rouletteplayertrans = CVAR_INIT ("rouletteonplayertrans", "7", CV_SAVE, rlplrtrans_cons_t, NULL);

// Item box visibility; a separate cvar
consvar_t cv_rouletteplayerboxtrans = CVAR_INIT ("rouletteonplayerboxtrans", "10", CV_SAVE, rlplrtrans_cons_t, NULL);

consvar_t cv_showviewpointtext = CVAR_INIT ("showviewpointtext", "On", CV_SAVE, CV_OnOff, NULL);

// Intermission time Tails 04-19-2002
static CV_PossibleValue_t inttime_cons_t[] = {{0, "MIN"}, {3600, "MAX"}, {0, NULL}};
consvar_t cv_inttime = CVAR_INIT ("inttime", "10", CV_SAVE|CV_NETVAR, inttime_cons_t, NULL);
consvar_t cv_rolltime = CVAR_INIT ("rolltime", "10", CV_SAVE|CV_NETVAR, inttime_cons_t, NULL);

static CV_PossibleValue_t advancemap_cons_t[] = {{0, "Same"}, {1, "Next"}, {2, "Random"}, {3, "Vote"}, {0, NULL}};
consvar_t cv_advancemap = CVAR_INIT ("advancemap", "Vote", CV_NETVAR, advancemap_cons_t, NULL);

consvar_t cv_runscripts = CVAR_INIT ("runscripts", "Yes", 0, CV_YesNo, NULL);

consvar_t cv_pause = CVAR_INIT ("pausepermission", "Server", CV_SAVE|CV_NETVAR, pause_cons_t, NULL);
consvar_t cv_mute = CVAR_INIT ("mute", "Off", CV_NETVAR|CV_CALL, CV_OnOff, Mute_OnChange);

consvar_t cv_sleep = CVAR_INIT ("cpusleep", "1", CV_SAVE, sleeping_cons_t, NULL);

static CV_PossibleValue_t encorevotes_cons_t[] = {{0, "One"}, {1, "Except One"}, {0, NULL}};
consvar_t cv_encorevotes = CVAR_INIT ("kartencorevotes", "One", CV_NETVAR, encorevotes_cons_t, NULL);

static CV_PossibleValue_t perfstats_cons_t[] = {
	{PS_OFF, "Off"},
	{PS_RENDER, "Rendering"},
	{PS_LOGIC, "Logic"},
	{PS_BOT, "Bots"},
	{PS_THINKFRAME, "ThinkFrame"},
	{0, NULL}
};
consvar_t cv_perfstats = CVAR_INIT ("perfstats", "Off", 0, perfstats_cons_t, NULL);

consvar_t cv_director = CVAR_INIT ("director", "Off", 0, CV_OnOff, NULL);

consvar_t cv_schedule = CVAR_INIT ("schedule", "On", CV_NETVAR|CV_CALL, CV_OnOff, Schedule_OnChange);

consvar_t cv_automate = CVAR_INIT ("automate", "On", CV_NETVAR, CV_OnOff, NULL);

consvar_t cv_debugmappatch = CVAR_INIT ("debugmappatch", "", 0, NULL, NULL);

char timedemo_name[256];
boolean timedemo_csv;
char timedemo_csv_id[256];
boolean timedemo_quit;

INT16 gametype = GT_RACE;
INT16 numgametypes = GT_FIRSTFREESLOT;

boolean forceresetplayers = false;
boolean deferencoremode = false;
UINT8 splitscreen = 0;
boolean circuitmap = false;
INT32 adminplayers[MAXPLAYERS] = {};

#define VOTEROWS ((cv_votemaxrows.value*3) + ((cv_votemaxrows.value > 1) ? (cv_votemaxrows.value - 1) : 0))
#define VOTEROWSADDSONE ((cv_votemaxrows.value*3) + 1 + ((cv_votemaxrows.value > 1) ? (cv_votemaxrows.value - 1) : 0))

// Scheduled commands.
scheduleTask_t **schedule = NULL;
size_t schedule_size = 0;
size_t schedule_len = 0;

// Automation commands
char *automate_commands[AEV__MAX];

const char *automate_names[AEV__MAX] =
{
	"RoundStart", // AEV_ROUNDSTART
	"IntermissionStart", // AEV_INTERMISSIONSTART
	"VoteStart" // AEV_VOTESTART
};

/// \warning Keep this up-to-date if you add/remove/rename net text commands
const char *netxcmdnames[MAXNETXCMD - 1] =
{
	"NAMEANDCOLOR", // XD_NAMEANDCOLOR
	"WEAPONPREF", // XD_WEAPONPREF
	"KICK", // XD_KICK
	"NETVAR", // XD_NETVAR
	"SAY", // XD_SAY
	"MAP", // XD_MAP
	"EXITLEVEL", // XD_EXITLEVEL
	"ADDFILE", // XD_ADDFILE
	"PAUSE", // XD_PAUSE
	"ADDPLAYER", // XD_ADDPLAYER
	"TEAMCHANGE", // XD_TEAMCHANGE
	"CLEARSCORES", // XD_CLEARSCORES
	"VERIFIED", // XD_VERIFIED
	"RANDOMSEED", // XD_RANDOMSEED
	"RUNSOC", // XD_RUNSOC
	"REQADDFILE", // XD_REQADDFILE
	"SETMOTD", // XD_SETMOTD
	"RESPAWN", // XD_RESPAWN
	"DEMOTED", // XD_DEMOTED
	"LUACMD", // XD_LUACMD
	"LUAVAR", // XD_LUAVAR
	"LUAFILE", // XD_LUAFILE

	// SRB2Kart
	"SETUPVOTE", // XD_SETUPVOTE
	"MODIFYVOTE", // XD_MODIFYVOTE
	"PICKVOTE", // XD_PICKVOTE
	"REMOVEPLAYER", // XD_REMOVEPLAYER
	"POWERLEVEL", // XD_POWERLEVEL
	"PARTYINVITE", // XD_PARTYINVITE
	"ACCEPTPARTYINVITE", // XD_ACCEPTPARTYINVITE
	"LEAVEPARTY", // XD_LEAVEPARTY
	"CANCELPARTYINVITE", // XD_CANCELPARTYINVITE
	"ADDBOT", // XD_ADDBOT
	"DISCORD", // XD_DISCORD
	"PLAYSOUND", // XD_PLAYSOUND
	"SCHEDULETASK", // XD_SCHEDULETASK
	"SCHEDULECLEAR", // XD_SCHEDULECLEAR
	"AUTOMATE", // XD_AUTOMATE
	"CHEAT", // XD_CHEAT
};

// =========================================================================
//                           SERVER STARTUP
// =========================================================================

/** Registers server commands and variables.
  * Anything required by a dedicated server should probably go here.
  *
  * \sa D_RegisterClientCommands
  */
void D_RegisterServerCommands(void)
{
	INT32 i;
	Forceskin_cons_t[0].value = -1;
	Forceskin_cons_t[0].strvalue = "Off";

	G_UpdateGametypeSelections();

	// Set the values to 0/NULL, it will be overwritten later when a skin is assigned to the slot.
	for (i = 1; i < MAXSKINS; i++)
	{
		Forceskin_cons_t[i].value = 0;
		Forceskin_cons_t[i].strvalue = NULL;
	}
	RegisterNetXCmd(XD_NAMEANDCOLOR, Got_NameAndColor);
	RegisterNetXCmd(XD_WEAPONPREF, Got_WeaponPref);
	RegisterNetXCmd(XD_POWERLEVEL, Got_PowerLevel);
	RegisterNetXCmd(XD_PARTYINVITE, Got_PartyInvite);
	RegisterNetXCmd(XD_ACCEPTPARTYINVITE, Got_AcceptPartyInvite);
	RegisterNetXCmd(XD_CANCELPARTYINVITE, Got_CancelPartyInvite);
	RegisterNetXCmd(XD_LEAVEPARTY, Got_LeaveParty);
	RegisterNetXCmd(XD_MAP, Got_Mapcmd);
	RegisterNetXCmd(XD_EXITLEVEL, Got_ExitLevelcmd);
	RegisterNetXCmd(XD_ADDFILE, Got_Addfilecmd);
	RegisterNetXCmd(XD_REQADDFILE, Got_RequestAddfilecmd);
	RegisterNetXCmd(XD_PAUSE, Got_Pause);
	RegisterNetXCmd(XD_RESPAWN, Got_Respawn);
	RegisterNetXCmd(XD_RUNSOC, Got_RunSOCcmd);
	RegisterNetXCmd(XD_LUACMD, Got_Luacmd);
	RegisterNetXCmd(XD_LUAFILE, Got_LuaFile);

	RegisterNetXCmd(XD_SETUPVOTE, Got_SetupVotecmd);
	RegisterNetXCmd(XD_MODIFYVOTE, Got_ModifyVotecmd);
	RegisterNetXCmd(XD_PICKVOTE, Got_PickVotecmd);

	RegisterNetXCmd(XD_SCHEDULETASK, Got_ScheduleTaskcmd);
	RegisterNetXCmd(XD_SCHEDULECLEAR, Got_ScheduleClearcmd);
	RegisterNetXCmd(XD_AUTOMATE, Got_Automatecmd);

	RegisterNetXCmd(XD_CHEAT, Got_Cheat);
	COM_AddCommand("scoreboard_addline", Command_ScoreboardAdd);
	COM_AddCommand("scoreboard_clear", Command_ScoreboardClear);

	// Remote Administration
	COM_AddCommand("password", Command_Changepassword_f);
	COM_AddCommand("clearpassword", Command_Clearpassword_f);
	COM_AddCommand("login", Command_Login_f); // useful in dedicated to kick off remote admin
	COM_AddCommand("promote", Command_Verify_f);
	RegisterNetXCmd(XD_VERIFIED, Got_Verification);
	COM_AddCommand("demote", Command_RemoveAdmin_f);
	RegisterNetXCmd(XD_DEMOTED, Got_Removal);

	COM_AddCommand("motd", Command_MotD_f);
	RegisterNetXCmd(XD_SETMOTD, Got_MotD_f); // For remote admin

	RegisterNetXCmd(XD_TEAMCHANGE, Got_Teamchange);
	COM_AddCommand("serverchangeteam", Command_ServerTeamChange_f);

	RegisterNetXCmd(XD_CLEARSCORES, Got_Clearscores);
	COM_AddCommand("clearscores", Command_Clearscores_f);
	COM_AddCommand("map", Command_Map_f);
	COM_AddCommand("randommap", Command_RandomMap);
	COM_AddCommand("restartlevel", Command_RestartLevel);

	COM_AddCommand("exitgame", Command_ExitGame_f);
	COM_AddCommand("retry", Command_Retry_f);
	COM_AddCommand("exitlevel", Command_ExitLevel_f);
	COM_AddCommand("showmap", Command_Showmap_f);
	COM_AddCommand("maphash", Command_Maphash_f);

	COM_AddCommand("addfilelocal", Command_Addfilelocal);
	COM_AddCommand("addfile", Command_Addfile);
	COM_AddCommand("listwad", Command_ListWADS_f);
	COM_AddCommand("locatelump", Command_LocateLump_f);
	COM_AddCommand("listmapthings", Command_ListDoomednums_f);

	COM_AddCommand("runsoc", Command_RunSOC);
	COM_AddCommand("pause", Command_Pause);

	COM_AddCommand("respawn", Command_Respawn);
	COM_AddCommand("respawn2", Command_Respawn2);
	COM_AddCommand("respawn3", Command_Respawn3);
	COM_AddCommand("respawn4", Command_Respawn4);

	COM_AddCommand("gametype", Command_ShowGametype_f);
	COM_AddCommand("version", Command_Version_f);
#ifdef UPDATE_ALERT
	COM_AddCommand("mod_details", Command_ModDetails_f);
#endif
	COM_AddCommand("quit", Command_Quit_f);

	COM_AddCommand("saveconfig", Command_SaveConfig_f);
	COM_AddCommand("loadconfig", Command_LoadConfig_f);
	COM_AddCommand("changeconfig", Command_ChangeConfig_f);
	COM_AddCommand("isgamemodified", Command_Isgamemodified_f); // test
	COM_AddCommand("showscores", Command_ShowScores_f);
	COM_AddCommand("showtime", Command_ShowTime_f);
	COM_AddCommand("cheats", Command_Cheats_f); // test
#ifdef _DEBUG
	COM_AddCommand("togglemodified", Command_Togglemodified_f);
	COM_AddCommand("archivetest", Command_Archivetest_f);
#endif

	COM_AddCommand("replaymarker", Command_ReplayMarker);

	COM_AddCommand("downloads", Command_Downloads_f);

	COM_AddCommand("kartgiveitem", Command_KartGiveItem_f);

	COM_AddCommand("schedule_add", Command_Schedule_Add);
	COM_AddCommand("schedule_clear", Command_Schedule_Clear);
	COM_AddCommand("schedule_list", Command_Schedule_List);

	COM_AddCommand("automate_set", Command_Automate_Set);

	// for master server connection
	AddMServCommands();

	// p_mobj.c
	CV_RegisterVar(&cv_itemrespawntime);
	CV_RegisterVar(&cv_itemrespawn);

	// misc
	CV_RegisterVar(&cv_friendlyfire);
	CV_RegisterVar(&cv_pointlimit);
	CV_RegisterVar(&cv_numlaps);

	CV_RegisterVar(&cv_autobalance);
	CV_RegisterVar(&cv_teamscramble);
	CV_RegisterVar(&cv_scrambleonchange);

	CV_RegisterVar(&cv_inttime);
	CV_RegisterVar(&cv_rolltime);
	CV_RegisterVar(&cv_advancemap);
	CV_RegisterVar(&cv_timelimit);
	CV_RegisterVar(&cv_playbackspeed);
	CV_RegisterVar(&cv_forceskin);
	CV_RegisterVar(&cv_downloading);
	CV_RegisterVar(&cv_votemaxrows);

	K_RegisterKartStuff(); // SRB2kart

	CV_RegisterVar(&cv_startinglives);
	CV_RegisterVar(&cv_countdowntime);
	CV_RegisterVar(&cv_runscripts);
	CV_RegisterVar(&cv_overtime);
	CV_RegisterVar(&cv_pause);
	CV_RegisterVar(&cv_mute);

	RegisterNetXCmd(XD_RANDOMSEED, Got_RandomSeed);

	CV_RegisterVar(&cv_allowexitlevel);
	CV_RegisterVar(&cv_restrictskinchange);
	CV_RegisterVar(&cv_allowteamchange);
	CV_RegisterVar(&cv_ingamecap);
	CV_RegisterVar(&cv_spectatorreentry);
	CV_RegisterVar(&cv_antigrief);
	CV_RegisterVar(&cv_respawntime);

	// d_clisrv
	CV_RegisterVar(&cv_maxplayers);
	CV_RegisterVar(&cv_joindelay);
	CV_RegisterVar(&cv_resynchattempts);
	CV_RegisterVar(&cv_maxsend);
	CV_RegisterVar(&cv_noticedownload);
	CV_RegisterVar(&cv_downloadspeed);
	CV_RegisterVar(&cv_httpsource);
	CV_RegisterVar(&cv_allownewplayer);
#ifdef VANILLAJOINNEXTROUND
	CV_RegisterVar(&cv_joinnextround);
#endif
	CV_RegisterVar(&cv_showjoinaddress);
	CV_RegisterVar(&cv_shownodeip);
	CV_RegisterVar(&cv_blamecfail);

	COM_AddCommand("ping", Command_Ping_f);
	CV_RegisterVar(&cv_nettimeout);
	CV_RegisterVar(&cv_jointimeout);
	CV_RegisterVar(&cv_kicktime);
	CV_RegisterVar(&cv_skipmapcheck);
	CV_RegisterVar(&cv_sleep);
	CV_RegisterVar(&cv_maxping);
	CV_RegisterVar(&cv_lagless);
	CV_RegisterVar(&cv_pingtimeout);
	CV_RegisterVar(&cv_showping);
	CV_RegisterVar(&cv_pingmeasurement);
	CV_RegisterVar(&cv_pingicon);
	CV_RegisterVar(&cv_pingstyle);
	CV_RegisterVar(&cv_showminimapnames);
	CV_RegisterVar(&cv_showminimapangle);
	CV_RegisterVar(&cv_minihead);
	CV_RegisterVar(&cv_spinoutroll);
	CV_RegisterVar(&cv_rouletteonplayer);
	CV_RegisterVar(&cv_rouletteplayertrans);
	CV_RegisterVar(&cv_rouletteplayerboxtrans);
	CV_RegisterVar(&cv_showlapemblem);
	CV_RegisterVar(&cv_lapemblemmode);
	CV_RegisterVar(&cv_racesplits);
	CV_RegisterVar(&cv_showviewpointtext);

	CV_RegisterVar(&cv_schedule);
	CV_RegisterVar(&cv_automate);

	CV_RegisterVar(&cv_dummyconsvar);
	CV_RegisterVar(&cv_encorevotes);

	CV_RegisterVar(&cv_itemlist);

#ifdef USE_STUN
	CV_RegisterVar(&cv_stunserver);
#endif

	CV_RegisterVar(&cv_discordinvites);
	RegisterNetXCmd(XD_DISCORD, Got_DiscordInfo);

	COM_AddCommand("numthinkers", Command_Numthinkers_f);
	COM_AddCommand("countmobjs", Command_CountMobjs_f);

	CV_RegisterVar(&cv_recordmultiplayerdemos);
	CV_RegisterVar(&cv_netdemosyncquality);
	CV_RegisterVar(&cv_netdemosize);

	CV_RegisterVar(&cv_shoutname);
	CV_RegisterVar(&cv_shoutcolor);
	CV_RegisterVar(&cv_autoshout);

#ifdef _DEBUG
	COM_AddCommand("causecfail", Command_CauseCfail_f);
#endif
#ifdef LUA_ALLOW_BYTECODE
	COM_AddCommand("dumplua", Command_Dumplua_f);
#endif

	CV_RegisterVar(&cv_debugmappatch);

	COM_AddCommand("playsound", Command_PlaySound);
	RegisterNetXCmd(XD_PLAYSOUND, Got_PlaySound);

	// player restat
	CV_RegisterVar(&cv_allowrestat);
	CV_RegisterVar(&cv_notifyrestat);
}

// =========================================================================
//                           CLIENT STARTUP
// =========================================================================

/** Registers client commands and variables.
  * Nothing needed for a dedicated server should be registered here.
  *
  * \sa D_RegisterServerCommands
  */
void D_RegisterClientCommands(void)
{
	INT32 i;

	for (i = 0; i < MAXSKINCOLORS; i++)
	{
		Color_cons_t[i].value = i;
		Color_cons_t[i].strvalue = skincolors[i].name;
	}

	for (i = 3; i < MAXSKINCOLORS; i++)
	{
		Followercolor_cons_t[i].value = i-3;
		Followercolor_cons_t[i].strvalue = skincolors[i-3].name;
	}

	Followercolor_cons_t[2].value = FOLLOWERCOLOR_MATCH;
	Followercolor_cons_t[2].strvalue = "Match"; // Add "Match" option, which will make the follower color match the player's

	Followercolor_cons_t[1].value = FOLLOWERCOLOR_OPPOSITE;
	Followercolor_cons_t[1].strvalue = "Opposite"; // Add "Opposite" option, ...which is like "Match", but for coloropposite.

	Followercolor_cons_t[0].value = FOLLOWERCOLOR_DEFAULT;
	Followercolor_cons_t[0].strvalue = "Default"; // Add "Default" option, which will use the default color of the selected follower

	Color_cons_t[MAXSKINCOLORS].value = Followercolor_cons_t[MAXSKINCOLORS+2].value = 0;
	Color_cons_t[MAXSKINCOLORS].strvalue = Followercolor_cons_t[MAXSKINCOLORS+2].strvalue = NULL;

	// Set default player names
	// Monster Iestyn (12/08/19): not sure where else I could have actually put this, but oh well
	for (i = 0; i < MAXPLAYERS; i++)
		sprintf(player_names[i], "Player %c", 'A' + i); // SRB2Kart: Letters like Sonic 3!

	if (dedicated)
		return;

	COM_AddCommand("changeteam", Command_Teamchange_f);
	COM_AddCommand("changeteam2", Command_Teamchange2_f);
	COM_AddCommand("changeteam3", Command_Teamchange3_f);
	COM_AddCommand("changeteam4", Command_Teamchange4_f);

	COM_AddCommand("invite", Command_Invite_f);
	COM_AddCommand("cancelinvite", Command_CancelInvite_f);
	COM_AddCommand("acceptinvite", Command_AcceptInvite_f);
	COM_AddCommand("rejectinvite", Command_RejectInvite_f);
	COM_AddCommand("leaveparty", Command_LeaveParty_f);

	COM_AddCommand("playdemo", Command_Playdemo_f);
	COM_AddCommand("timedemo", Command_Timedemo_f);
	COM_AddCommand("stopdemo", Command_Stopdemo_f);
	COM_AddCommand("playintro", Command_Playintro_f);
	CV_RegisterVar(&cv_resyncdemo);
	CV_RegisterVar(&cv_resetnetvars);

	COM_AddCommand("resetcamera", Command_ResetCamera_f);

	COM_AddCommand("view", Command_View_f);
	COM_AddCommand("view2", Command_View_f);
	COM_AddCommand("view3", Command_View_f);
	COM_AddCommand("view4", Command_View_f);

	COM_AddCommand("setviews", Command_SetViews_f);

	COM_AddCommand("setcontrol", Command_Setcontrol_f);
	COM_AddCommand("setcontrol2", Command_Setcontrol2_f);
	COM_AddCommand("setcontrol3", Command_Setcontrol3_f);
	COM_AddCommand("setcontrol4", Command_Setcontrol4_f);

	COM_AddCommand("screenshot", M_ScreenShot);
	COM_AddCommand("startmovie", Command_StartMovie_f);
	COM_AddCommand("stopmovie", Command_StopMovie_f);
	COM_AddCommand("minigen", M_MinimapGenerate);

	CV_RegisterVar(&cv_screenshot_option);
	CV_RegisterVar(&cv_screenshot_folder);
	CV_RegisterVar(&cv_screenshot_colorprofile);
	CV_RegisterVar(&cv_moviemode);
	CV_RegisterVar(&cv_movie_option);
	CV_RegisterVar(&cv_movie_folder);
	// PNG variables
	CV_RegisterVar(&cv_zlib_level);
	CV_RegisterVar(&cv_zlib_memory);
	CV_RegisterVar(&cv_zlib_strategy);
	CV_RegisterVar(&cv_zlib_window_bits);
	// APNG variables
	CV_RegisterVar(&cv_zlib_levela);
	CV_RegisterVar(&cv_zlib_memorya);
	CV_RegisterVar(&cv_zlib_strategya);
	CV_RegisterVar(&cv_zlib_window_bitsa);
	CV_RegisterVar(&cv_apng_delay);
	CV_RegisterVar(&cv_apng_downscale);
	// GIF variables
	CV_RegisterVar(&cv_gif_optimize);
	CV_RegisterVar(&cv_gif_downscale);
	CV_RegisterVar(&cv_gif_dynamicdelay);
	CV_RegisterVar(&cv_gif_localcolortable);

	// register these so it is saved to config
	for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
	{
		CV_RegisterVar(&cv_playername[i]);
		CV_RegisterVar(&cv_playercolor[i]);
		CV_RegisterVar(&cv_skin[i]);
		CV_RegisterVar(&cv_follower[i]);
		CV_RegisterVar(&cv_followercolor[i]);
		CV_RegisterVar(&cv_jitterlegacy[i]);
		CV_RegisterVar(&cv_driftmode[i]);
		CV_RegisterVar(&cv_voice[i]);
		
		CV_RegisterVar(&cv_dummyrestatspeed[i]);
		CV_RegisterVar(&cv_dummyrestatweight[i]);
		CV_RegisterVar(&cv_dummyrestatrandom[i]);
	}

	COM_AddCommand("restat", Command_Restat);
	COM_AddCommand("restat2", Command_Restat2);
	COM_AddCommand("restat3", Command_Restat3);
	COM_AddCommand("restat4", Command_Restat4);

	// preferred number of players
	CV_RegisterVar(&cv_splitplayers);

	CV_RegisterVar(&cv_rollingdemos);
	CV_RegisterVar(&cv_netstat);
	CV_RegisterVar(&cv_netticbuffer);
	CV_RegisterVar(&cv_mindelay);

	// HUD
	CV_RegisterVar(&cv_itemfinder);

	CV_RegisterVar(&cv_emotes);

	// time attack ghost options are also saved to config
	CV_RegisterVar(&cv_ghost_besttime);
	CV_RegisterVar(&cv_ghost_bestlap);
	CV_RegisterVar(&cv_ghost_last);
	CV_RegisterVar(&cv_ghost_guest);
	CV_RegisterVar(&cv_ghost_staff);

	COM_AddCommand("displayplayer", Command_Displayplayer_f);

	// FIXME: not to be here.. but needs be done for config loading
#ifdef BACKWARDSCOMPATCORRECTION
	CV_RegisterVar(&cv_globalgamma);
#endif
	CV_RegisterVar(&cv_globalbrightness);
	CV_RegisterVar(&cv_globalsaturation);

	CV_RegisterVar(&cv_rhue);
	CV_RegisterVar(&cv_yhue);
	CV_RegisterVar(&cv_ghue);
	CV_RegisterVar(&cv_chue);
	CV_RegisterVar(&cv_bhue);
	CV_RegisterVar(&cv_mhue);

	CV_RegisterVar(&cv_rbrightness);
	CV_RegisterVar(&cv_ybrightness);
	CV_RegisterVar(&cv_gbrightness);
	CV_RegisterVar(&cv_cbrightness);
	CV_RegisterVar(&cv_bbrightness);
	CV_RegisterVar(&cv_mbrightness);

	CV_RegisterVar(&cv_rsaturation);
	CV_RegisterVar(&cv_ysaturation);
	CV_RegisterVar(&cv_gsaturation);
	CV_RegisterVar(&cv_csaturation);
	CV_RegisterVar(&cv_bsaturation);
	CV_RegisterVar(&cv_msaturation);
	CV_RegisterVar(&cv_palette);
	CV_RegisterVar(&cv_palettenum);

	// m_menu.c
	//CV_RegisterVar(&cv_compactscoreboard);
	CV_RegisterVar(&cv_chatheight);
	CV_RegisterVar(&cv_chatwidth);
	CV_RegisterVar(&cv_chattime);
	CV_RegisterVar(&cv_chatspamprotection);
	CV_RegisterVar(&cv_consolechat);
	CV_RegisterVar(&cv_chatnotifications);
	CV_RegisterVar(&cv_chatbacktint);

	CV_RegisterVar(&cv_growmusic);
	CV_RegisterVar(&cv_supermusic);
	CV_RegisterVar(&cv_altshrinkmusic);

	CV_RegisterVar(&cv_songcredits);
	CV_RegisterVar(&cv_tutorialprompt);
	CV_RegisterVar(&cv_showfocuslost);
	CV_RegisterVar(&cv_pauseifunfocused);

	// g_input.c
	for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
	{
		CV_RegisterVar(&cv_kickstartaccel[i]);
		CV_RegisterVar(&cv_shrinkme[i]);
		CV_RegisterVar(&cv_deadzonex[i]);
		CV_RegisterVar(&cv_deadzoney[i]);
		CV_RegisterVar(&cv_deadzonet[i]);
		CV_RegisterVar(&cv_deadzonestyle[i]);
		CV_RegisterVar(&cv_litesteer[i]);
		CV_RegisterVar(&cv_turnsmooth[i]);
		CV_RegisterVar(&cv_controllerrumble[i]);
		CV_RegisterVar(&cv_rumblestrength[i]);
		CV_RegisterVar(&cv_controllerled[i]);
		CV_RegisterVar(&cv_tiltcontrol[i]);
	}

	// filesrch.c
	CV_RegisterVar(&cv_addons_option);
	CV_RegisterVar(&cv_addons_folder);
	CV_RegisterVar(&cv_addons_md5);
	CV_RegisterVar(&cv_addons_showall);
	CV_RegisterVar(&cv_addons_search_type);
	CV_RegisterVar(&cv_addons_search_case);

	CV_RegisterVar(&cv_controlperkey);

	CV_RegisterVar(&cv_usemouse);
	CV_RegisterVar(&cv_invertmouse);
	CV_RegisterVar(&cv_mousesens);
	CV_RegisterVar(&cv_mouseysens);
	//CV_RegisterVar(&cv_mousemove);

	for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
	{
		CV_RegisterVar(&cv_usecontroller[i]);
		CV_RegisterVar(&cv_controllerscale[i]);
	}

	// s_sound.c
	CV_RegisterVar(&cv_soundvolume);
	CV_RegisterVar(&cv_closedcaptioning);
	CV_RegisterVar(&cv_digmusicvolume);
	CV_RegisterVar(&cv_numChannels);

	// al_sound.c
	CV_RegisterVar(&cv_soundefx);

	// screen.c
	CV_RegisterVar(&cv_fullscreen);
	CV_RegisterVar(&cv_renderview);
	CV_RegisterVar(&cv_vhseffect);
	CV_RegisterVar(&cv_shittyscreen);
	CV_RegisterVar(&cv_votebgscaling);
	CV_RegisterVar(&cv_renderer);
	CV_RegisterVar(&cv_scr_depth);
	CV_RegisterVar(&cv_scr_width);
	CV_RegisterVar(&cv_scr_height);
	CV_RegisterVar(&cv_parallelsoftware);

	CV_RegisterVar(&cv_director);

	CV_RegisterVar(&cv_soundtest);

	CV_RegisterVar(&cv_invincmusicfade);
	CV_RegisterVar(&cv_growmusicfade);
	CV_RegisterVar(&cv_altshrinkmusicfade);

	CV_RegisterVar(&cv_resetspecialmusic);

	CV_RegisterVar(&cv_resume);
	CV_RegisterVar(&cv_perfstats);

	//Value used to store last server player has joined
	CV_RegisterVar(&cv_lastserver);

	CV_RegisterVar(&cv_laglesscam);

	// ingame object placing
	COM_AddCommand("objectplace", Command_ObjectPlace_f);
	// //COM_AddCommand("writethings", Command_Writethings_f);
	CV_RegisterVar(&cv_speed);
	CV_RegisterVar(&cv_opflags);
	CV_RegisterVar(&cv_ophoopflags);
	CV_RegisterVar(&cv_mapthingnum);
//	CV_RegisterVar(&cv_grid);
//	CV_RegisterVar(&cv_snapto);

	// add cheats
	COM_AddCommand("noclip", Command_CheatNoClip_f);
	COM_AddCommand("god", Command_CheatGod_f);
	COM_AddCommand("setrings", Command_Setrings_f);
	COM_AddCommand("setlives", Command_Setlives_f);
	COM_AddCommand("setscore", Command_Setscore_f);
	COM_AddCommand("devmode", Command_Devmode_f);
	COM_AddCommand("savecheckpoint", Command_Savecheckpoint_f);
	COM_AddCommand("scale", Command_Scale_f);
	COM_AddCommand("gravflip", Command_Gravflip_f);
	COM_AddCommand("hurtme", Command_Hurtme_f);
	COM_AddCommand("ateleport", Command_ATeleport_f);
	COM_AddCommand("teleport", Command_Teleport_f);
	COM_AddCommand("rteleport", Command_RTeleport_f);
	COM_AddCommand("skynum", Command_Skynum_f);
	COM_AddCommand("weather", Command_Weather_f);
	COM_AddCommand("angle", Command_Angle_f);
	CV_RegisterVar(&cv_renderhitbox);
	CV_RegisterVar(&cv_showgremlins);

#ifdef HAVE_DISCORDRPC
	CV_RegisterVar(&cv_discordrp);
	CV_RegisterVar(&cv_discordstreamer);
	CV_RegisterVar(&cv_discordasks);
#endif

	COM_AddCommand("listskins", Command_ListSkins);
	COM_AddCommand("listvoices", Command_ListVoices);

	CV_RegisterVar(&cv_connectawaittime);
	CV_RegisterVar(&cv_serverinfoscreen);
}

static void RestatForPlayer(UINT32 ssplayer)
{
	if (!cv_allowrestat.value)
	{
		CONS_Printf("This command has been disabled by the server host.\n");
		return;
	}

	if (COM_Argc() > 2)
	{
		int speed;
		int weight;

		if (sscanf(COM_Argv(1), " %d", &speed) == 0 || 
			sscanf(COM_Argv(2), " %d", &weight) == 0)
		{
			CONS_Printf("Expected two numbers (<speed> <weight>) in the range of 1-9.\n");
			return;
		}
		// range checking
		if (speed < 1 || speed > 9 ||
			weight < 1 || weight > 9)
		{
			CONS_Printf("Expected two numbers (<speed> <weight>) in the range of 1-9.\n");
			return;	
		}

		// should be good now 🥲
		CONS_Printf("You will be %d speed, %d weight for your next race.\n", speed, weight);
		CONS_Printf("Use \"restat off\" to return to your skin's default stats.\n");
		CV_StealthSetValue(&cv_dummyrestatspeed[ssplayer], speed);
		CV_StealthSetValue(&cv_dummyrestatweight[ssplayer], weight);
		CV_StealthSetValue(&cv_dummyrestatrandom[ssplayer], false);

		// We'll handle it later if we're not playing.
		if (!Playing())
			return;

		if (ssplayer > 0 && !splitscreen)
			return;

		WeaponPref_Send(ssplayer);
		return;
	}
	else if (COM_Argc() > 1)
	{
		if (fasticmp(COM_Argv(1), "off"))
		{
			CONS_Printf("Now using skin default stats.\n");
			CV_StealthSetValue(&cv_dummyrestatspeed[ssplayer], 0);
			CV_StealthSetValue(&cv_dummyrestatweight[ssplayer], 0);
			CV_StealthSetValue(&cv_dummyrestatrandom[ssplayer], false);

			// We'll handle it later if we're not playing.
			if (!Playing())
				return;

			if (ssplayer > 0 && !splitscreen)
				return;

			WeaponPref_Send(ssplayer);
			return;
		}
		else if (fasticmp(COM_Argv(1), "random"))
		{
			CV_StealthSetValue(&cv_dummyrestatrandom[ssplayer], !cv_dummyrestatrandom[ssplayer].value);
			if (cv_dummyrestatrandom[ssplayer].value)
			{
				CONS_Printf("Random restat is now enabled.\n");
			}
			else
			{
				CONS_Printf("Random restat is now disabled.\n");
			}

			// We'll handle it later if we're not playing.
			if (!Playing())
				return;

			if (ssplayer > 0 && !splitscreen)
				return;

			WeaponPref_Send(ssplayer);
			return;
		}
		else
		{
			CONS_Printf(
				"Usage: \"restat<playernum> <speed> <weight>\"\n"
				"Alternatively: \"restat<playernum> random\" to toggle the use of random stats each round.\n"
				"or \"restat<playernum> off\" to use your skin's default stats.\n");
		}
	}
	else
	{
		CONS_Printf(
			"Usage: \"restat<playernum> <speed> <weight>\"\n"
			"Alternatively: \"restat<playernum> random\" to use random stats each round.\n"
			"or \"restat<playernum> off\" to use your skin's default stats.\n");
	}
}

static void Command_Restat(void)
{
	RestatForPlayer(0);
}

static void Command_Restat2(void)
{
	RestatForPlayer(1);
}

static void Command_Restat3(void)
{
	RestatForPlayer(2);
}

static void Command_Restat4(void)
{
	RestatForPlayer(3);
}

/**
 * Searches the skins list for a skin of the "realname", if found, changes
 * the CVAR value to be the "name" of the skin.
 * \sa cv_skin, Skin_OnChange, Skin2_OnChange
 * \author karen
 */
static void Skin_FindRealNameSkin(consvar_t *cvar)
{
	// Not the best way to implements this but it will do.

	int i;
	const char *value = cvar->string;
	for (i = 0; i < numskins; i++)
	{
		if (strncmp(value, skins[i].realname, sizeof skins[i].realname) == 0)
		{
			// Change the cvar to be the value of the name.
			CV_StealthSet(cvar, skins[i].name);
			return;
		}
	}
}

/** Checks if a name (as received from another player) is okay.
  * A name is okay if it is no fewer than 1 and no more than ::MAXPLAYERNAME
  * chars long (not including NUL), it does not begin or end with a space,
  * it does not contain non-printing characters (according to isprint(), which
  * allows space), it does not start with a digit, and no other player is
  * currently using it.
  * \param name      Name to check.
  * \param playernum Player who wants the name, so we can check if they already
  *                  have it, and let them keep it if so.
  * \sa CleanupPlayerName, SetPlayerName, Got_NameAndColor
  * \author Graue <graue@oceanbase.org>
  */

static boolean AllowedPlayerNameChar(char ch)
{
	if (!isprint(ch) || ch == ';' || ch == '"' || (UINT8)(ch) >= 0x80)
		return false;

	return true;
}

boolean EnsurePlayerNameIsGood(char *name, INT32 playernum)
{
	INT32 ix;

	if (strlen(name) == 0 || strlen(name) > MAXPLAYERNAME)
		return false; // Empty or too long.
	if (name[0] == ' ' || name[strlen(name)-1] == ' ')
		return false; // Starts or ends with a space.
	if (isdigit(name[0]))
		return false; // Starts with a digit.
	if (name[0] == '@' || name[0] == '~')
		return false; // Starts with an admin symbol.

	// Check if it contains a non-printing character.
	// Note: ANSI C isprint() considers space a printing character.
	// Also don't allow semicolons, since they are used as
	// console command separators.

	// Also, anything over 0x80 is disallowed too, since compilers love to
	// differ on whether they're printable characters or not.
	for (ix = 0; name[ix] != '\0'; ix++)
		if (!AllowedPlayerNameChar(name[ix]))
			return false;

	// Check if a player is currently using the name, case-insensitively.
	for (ix = 0; ix < MAXPLAYERS; ix++)
	{
		if (ix != playernum && playeringame[ix]
			&& strcasecmp(name, player_names[ix]) == 0)
		{
			// We shouldn't kick people out just because
			// they joined the game with the same name
			// as someone else -- modify the name instead.
			size_t len = strlen(name);

			// Recursion!
			// Slowly strip characters off the end of the
			// name until we no longer have a duplicate.
			if (len > 1)
			{
				name[len-1] = '\0';
				if (!EnsurePlayerNameIsGood (name, playernum))
					return false;
			}
			else if (len == 1) // Agh!
			{
				// Last ditch effort...
				sprintf(name, "%d", M_RandomKey(10));
				if (!EnsurePlayerNameIsGood (name, playernum))
					return false;
			}
			else
				return false;
		}
	}

	return true;
}

/** Cleans up a local player's name before sending a name change.
  * Spaces at the beginning or end of the name are removed. Then if the new
  * name is identical to another player's name, ignoring case, the name change
  * is canceled, and the name in cv_playername[n].value
  * is restored to what it was before.
  *
  * We assume that if playernum is in ::g_localplayers
  * (unless clientjoin is true, a necessary evil)
  * the console variable ::cv_playername[n] is
  * already set to newname. However, the player name table is assumed to
  * contain the old name.
  *
  * \param playernum Player number who has requested a name change.
  *                  Should be in ::g_localplayers.
  * \param newname   New name for that player; should already be in
  *                  ::cv_playername if player is a local player.
  * \sa cv_playername, SendNameAndColor, SetPlayerName
  * \author Graue <graue@oceanbase.org>
  */
void CleanupPlayerName(INT32 playernum, const char *newname)
{
	char *buf;
	char *p;
	char *tmpname = NULL;
	INT32 i;
	boolean namefailed = true;
	boolean clientjoin = !!(playernum >= MAXPLAYERS);

	if (clientjoin)
		playernum -= MAXPLAYERS;

	buf = Z_StrDup(newname);

	do
	{
		p = buf;

		while (*p == ' ')
			p++; // remove leading spaces

		if (strlen(p) == 0)
			break; // empty names not allowed

		if (isdigit(*p))
			break; // names starting with digits not allowed

		if (*p == '@' || *p == '~')
			break; // names that start with @ or ~ (admin symbols) not allowed

		tmpname = p;

		do
		{
			if (!AllowedPlayerNameChar(*p))
				break;
		}
		while (*++p) ;

		if (*p)/* bad char found */
			break;

		// Remove trailing spaces.
		p = &tmpname[strlen(tmpname)-1]; // last character
		while (*p == ' ' && p >= tmpname)
		{
			*p = '\0';
			p--;
		}

		if (strlen(tmpname) == 0)
			break; // another case of an empty name

		// Truncate name if it's too long (max MAXPLAYERNAME chars
		// excluding NUL).
		if (strlen(tmpname) > MAXPLAYERNAME)
			tmpname[MAXPLAYERNAME] = '\0';

		// Remove trailing spaces again.
		p = &tmpname[strlen(tmpname)-1]; // last character
		while (*p == ' ' && p >= tmpname)
		{
			*p = '\0';
			p--;
		}

		// no stealing another player's name
		if (!clientjoin)
		{
			for (i = 0; i < MAXPLAYERS; i++)
			{
				if (i != playernum && playeringame[i]
					&& strcasecmp(tmpname, player_names[i]) == 0)
				{
					break;
				}
			}

			if (i < MAXPLAYERS)
				break;
		}

		// name is okay then
		namefailed = false;
	} while (0);

	if (namefailed)
		tmpname = player_names[playernum];

	// set consvars whether namefailed or not, because even if it succeeded,
	// spaces may have been removed
	if (clientjoin)
		CV_StealthSet(&cv_playername[playernum], tmpname);
	else
	{
		for (i = 0; i <= splitscreen; i++)
		{
			if (playernum == g_localplayers[i])
			{
				CV_StealthSet(&cv_playername[i], tmpname);
				break;
			}
		}

		if (i > splitscreen)
		{
			I_Assert(((void)"CleanupPlayerName used on non-local player", 0));
		}
	}

	Z_Free(buf);
}

/** Sets a player's name, if it is good.
  * If the name is not good (indicating a modified or buggy client), it is not
  * set, and if we are the server in a netgame, the player responsible is
  * kicked with a consistency failure.
  *
  * This function prints a message indicating the name change, unless the game
  * is currently showing the intro, e.g. when processing autoexec.cfg.
  *
  * \param playernum Player number who has requested a name change.
  * \param newname   New name for that player. Should be good, but won't
  *                  necessarily be if the client is maliciously modified or
  *                  buggy.
  * \sa CleanupPlayerName, EnsurePlayerNameIsGood
  * \author Graue <graue@oceanbase.org>
  */
static void SetPlayerName(INT32 playernum, char *newname)
{
	if (EnsurePlayerNameIsGood(newname, playernum))
	{
		if (strcasecmp(newname, player_names[playernum]) != 0)
		{
			if (!LUA_HookNameChange(&players[playernum], newname))
			{
				// Name change rejected by Lua
				if (playernum == consoleplayer)
					CV_StealthSet(cv_playername, player_names[consoleplayer]);
				return;
			}

			if (netgame)
				HU_AddChatText(va("\x82*%s renamed to %s", player_names[playernum], newname), false);

			player_name_changes[playernum]++;

			strcpy(player_names[playernum], newname);
			demo_extradata[playernum] |= DXD_NAME;
		}
	}
	else
	{
		CONS_Printf(M_GetText("Player %d sent a bad name change\n"), playernum+1);
		if (server && netgame)
			SendKick(playernum, KICK_MSG_CON_FAIL);
	}
}

UINT8 CanChangeSkin(INT32 playernum)
{
	// Of course we can change if we're not playing
	if (!Playing() || !addedtogame)
		return true;

	// Force skin in effect.
	if (cv_forceskin.value != -1)
		return false;

	// Can change skin in intermission and whatnot.
	if (gamestate != GS_LEVEL)
		return true;

	// Server has skin change restrictions.
	if (cv_restrictskinchange.value)
	{
		// Can change skin during initial countdown.
		if (leveltime < starttime)
			return true;

		// Not in game, so you can change
		if (players[playernum].spectator || players[playernum].playerstate == PST_DEAD || players[playernum].playerstate == PST_REBORN)
			return true;

		return false;
	}

	return true;
}

static void ForceAllSkins(INT32 forcedskin)
{
	INT32 i, j;

	if (demo.playback)
		return; // DXD_SKIN should handle all changes for us

	for (i = 0; i < MAXPLAYERS; ++i)
	{
		if (!playeringame[i])
			continue;

		SetPlayerSkinByNum(i, forcedskin);

		// If it's me (or my brother (or my sister (or my trusty pet dog))), set appropriate skin value in cv_skin
		if (dedicated) // But don't do this for dedicated servers, of course.
			continue;

		for (j = 0; j <= splitscreen; j++)
		{
			if (i == g_localplayers[j])
			{
				CV_StealthSet(&cv_skin[j], skins[forcedskin].name);
				break;
			}
		}
	}
}

static const char *
VaguePartyDescription (int playernum, int size, int default_color)
{
	static char party_description
		[1 + MAXPLAYERNAME + 1 + sizeof " and x others"];
	const char *name;
	name = player_names[playernum];
	/*
	less than check for the dumb compiler because I KNOW it'll
	complain about "writing x bytes into an area of y bytes"!!!
	*/
	if (size > 1 && size <= MAXSPLITSCREENPLAYERS)
	{
		sprintf(party_description,
				"\x83%s%c and %d other%s",
				name,
				default_color,
				( size - 1 ),
				( (size > 2) ? "s" : "" )
		);
	}
	else
	{
		sprintf(party_description,
				"\x83%s%c",
				name,
				default_color
		);
	}
	return party_description;
}

static INT32 snacpending[MAXSPLITSCREENPLAYERS] = {0,0,0,0};
static INT32 chmappending = 0;

// name, color, skin, or voice has changed
//
static void SendNameAndColor(UINT8 n)
{
	const INT32 playernum = g_localplayers[n];
	player_t *player = &players[playernum];
	kartvoice_t *voice;
	kartvoice_t *valuevoice;
	INT32 prevskin;

	char buf[MAXPLAYERNAME+12];
	char *p;

	UINT16 i = 0;

	if (splitscreen < n)
		return; // can happen if skin4/color4/name4 changed

	if (playernum == -1)
		return;

	p = buf;

	// don't allow inaccessible colors
	if (!skincolors[cv_playercolor[n].value].accessible)
	{
		if (player->skincolor && skincolors[player->skincolor].accessible)
			CV_StealthSetValue(&cv_playercolor[n], player->skincolor);
		else if (skins[player->skin].prefcolor && skincolors[skins[player->skin].prefcolor].accessible)
			CV_StealthSetValue(&cv_playercolor[n], skins[player->skin].prefcolor);
		else if (skincolors[atoi(cv_playercolor[n].defaultvalue)].accessible)
			CV_StealthSet(&cv_playercolor[n], cv_playercolor[n].defaultvalue);
		else
		{
			while (i<numskincolors && !skincolors[i].accessible)
				i++;
			CV_StealthSetValue(&cv_playercolor[n], (i != numskincolors) ? i : SKINCOLOR_BLUE);
		}
	}

	// ditto for follower colour:
	if (!cv_followercolor[n].value)
		CV_StealthSet(&cv_followercolor[n], "Default"); // set it to "Default". I don't care about your stupidity!

	// so like, this is sent before we even use anything like cvars or w/e so it's possible that follower is set to a pretty yikes value, so let's fix that before we send garbage that could crash the game:
	if (cv_follower[n].value >= numfollowers || cv_follower[n].value < -1)
		CV_StealthSet(&cv_follower[n], "-1");

	// Check the player's acoustics.
	voice = P_GetMobjVoice(player->mo);

	if (!voice)
	{
		// ...how?!
		voice = &skins[player->skin].voices[0];
	}

	if (fastcmp(cv_playername[n].string, player_names[playernum])
		&& cv_playercolor[n].value == player->skincolor
		&& fastcmp(cv_skin[n].string, skins[player->skin].name)
		&& cv_follower[n].value == player->followerskin
		&& cv_followercolor[n].value == player->followercolor
		&& fasticmp(cv_voice[n].string, voice->name))
		return;

	// We'll handle it later if we're not playing.
	if (!Playing())
		return;

	// If you're not in a netgame, merely update the skin, color, and name.
	if (!netgame)
	{
		INT32 foundskin, voxid;

		CleanupPlayerName(playernum, cv_playername[n].zstring);
		strcpy(player_names[playernum], cv_playername[n].zstring);

		player->skincolor = cv_playercolor[n].value;

		K_KartResetPlayerColor(player);

		// Update follower for local games:
		if (cv_follower[n].value >= -1)
			K_SetFollowerByNum(playernum, cv_follower[n].value);

		player->followercolor = cv_followercolor[n].value;

		if (metalrecording && n == 0)
		{ // Starring Metal Sonic as themselves, obviously.
			SetPlayerSkinByNum(playernum, 5);
			CV_StealthSet(&cv_skin[n], skins[5].name);
		}
		else if ((foundskin = R_SkinAvailable(cv_skin[n].string)) != -1 && R_SkinUsable(playernum, foundskin))
		{
			prevskin = cv_skin[n].value;

			cv_skin[n].value = foundskin;
			SetPlayerSkin(playernum, cv_skin[n].string);
			CV_StealthSet(&cv_skin[n], skins[cv_skin[n].value].name);

			// Reset the voice.
			if (prevskin != player->skin)
			{
				if (cv_voice[n].string)
				{
					voxid = R_FindIDForVoice(&skins[player->skin], cv_voice[n].string);

					if (voxid == MAXSKINVOICES)
					{
						// No dice; use the default voice.
						valuevoice = &skins[player->skin].voices[0];
					}
					else
					{

						valuevoice = &skins[player->skin].voices[voxid];
					}
				}
				else
				{
					// NULL string; default to the skin's default.
					valuevoice = &skins[player->skin].voices[0];
				}

				CV_StealthSet(&cv_voice[n], valuevoice->name);
			}
		}
		else
		{
			prevskin = player->skin;

			cv_skin[n].value = players[playernum].skin;
			CV_StealthSet(&cv_skin[n], skins[player->skin].name);
			// will always be same as current
			SetPlayerSkin(playernum, cv_skin[n].string);

			// Reset the voice.
			if (prevskin != player->skin)
			{
				if (cv_voice[n].string)
				{
					voxid = R_FindIDForVoice(&skins[player->skin], cv_voice[n].string);

					if (voxid == MAXSKINVOICES)
					{
						// No dice; use the default voice.
						valuevoice = &skins[player->skin].voices[0];
					}
					else
					{

						valuevoice = &skins[player->skin].voices[voxid];
					}
				}
				else
				{
					// NULL string; default to the skin's default.
					valuevoice = &skins[player->skin].voices[0];
				}

				CV_StealthSet(&cv_voice[n], valuevoice->name);
			}
		}

		// Need to update voices after the fact.
		if (!cv_voice[n].string)
		{
			CV_StealthSet(&cv_voice[n], skins[player->skin].voices[0].name);
		}

		SetPlayerVoice(playernum, cv_voice[n].string);

		valuevoice = P_GetMobjVoice(player->mo);

		if (valuevoice)
		{
			CV_StealthSet(&cv_voice[n], valuevoice->name);
		}
		else
		{
			// Get the voice from our ID.
			// No need to compare parents, 
			valuevoice = &skins[player->skin].voices[player->voice_id];

			if (valuevoice)
			{
				CV_StealthSet(&cv_voice[n], valuevoice->name);
			}
			else
			{
				// ...still nothing?
				CV_StealthSet(&cv_voice[n], skins[player->skin].voices[0].name);
			}
		}

		return;
	}

	snacpending[n]++;

	// Don't change name if muted
	if (player_name_changes[playernum] >= MAXNAMECHANGES)
	{
		CV_StealthSet(&cv_playername[n], player_names[playernum]);
		HU_AddChatText("\x85*You must wait to change your name again", false);
	}
	else if (cv_mute.value && !(server || IsPlayerAdmin(playernum)))
		CV_StealthSet(&cv_playername[n], player_names[playernum]);
	else // Cleanup name if changing it
		CleanupPlayerName(playernum, cv_playername[n].zstring);

	// Don't change skin if the server doesn't want you to.
	if (!CanChangeSkin(playernum))
	{
		CV_StealthSet(&cv_skin[n], skins[player->skin].name);

		if (R_FindIDForVoice(&skins[player->skin], cv_voice[n].string) == MAXSKINVOICES)
		{
			// Our voice is no longer valid, set it to that of our skin's.
			CV_StealthSet(&cv_voice[n], skins[player->skin].voices[0].name);
		}
	}

	// check if player has the skin loaded (cv_skin may have
	// the name of a skin that was available in the previous game)
	cv_skin[n].value = R_SkinAvailable(cv_skin[n].string);
	if ((cv_skin[n].value < 0) || !R_SkinUsable(playernum, cv_skin[n].value))
	{
		CV_StealthSet(&cv_skin[n], DEFAULTSKIN);
		cv_skin[n].value = 0;

		CV_StealthSet(&cv_voice[n], "sonic_voice");
		cv_voice[n].value = 0;
	}

	// Need to update voices after the fact.
	if (!cv_voice[n].string)
	{
		CV_StealthSet(&cv_voice[n], skins[cv_skin[n].value].voices[0].name);
	}

	if (R_FindIDForVoice(&skins[cv_skin[n].value], cv_voice[n].string) == MAXSKINVOICES)
	{
		// In the chance our current voice isn't valid,
		// rescan our current voice and send that over the network.
		valuevoice = P_GetMobjVoice(player->mo);

		if (valuevoice)
		{
			CV_StealthSet(&cv_voice[n], valuevoice->name);
		}
		else
		{
			// Get the voice from our ID.
			// No need to compare parents.
			valuevoice = &skins[cv_skin[n].value].voices[player->voice_id];

			if (valuevoice)
			{
				CV_StealthSet(&cv_voice[n], valuevoice->name);
			}
			else
			{
				// ...still nothing?
				CV_StealthSet(&cv_voice[n], skins[cv_skin[n].value].voices[0].name);
			}
		}
	}

	// After EVERYTHING, do one last check on our voice so we can set the value.
	INT32 cvar_voxid = R_FindIDForVoice(&skins[cv_skin[n].value], cv_voice[n].string);

	if (cvar_voxid == MAXSKINVOICES)
	{
		// ...huh?! Reset to the default.
		cv_voice[n].value = 0;
	}
	else
	{
		cv_voice[n].value = cvar_voxid;
	}

	// Finally write out the complete packet and send it off.
	WRITESTRINGN(p, cv_playername[n].zstring, MAXPLAYERNAME);
	WRITEUINT16(p, (UINT16)cv_playercolor[n].value);
	WRITEUINT16(p, (UINT16)cv_skin[n].value);
	WRITEINT32(p, (INT32)cv_follower[n].value);
	WRITEUINT16(p, (UINT16)cv_followercolor[n].value);
	WRITEUINT16(p, (UINT16)cv_voice[n].value);

	SendNetXCmdForPlayer(n, XD_NAMEANDCOLOR, buf, p - buf);
}

static void Got_NameAndColor(UINT8 **cp, INT32 playernum)
{
	player_t *p = &players[playernum];
	char name[MAXPLAYERNAME+1];
	UINT16 color, followercolor;
	UINT16 skin, voice;
	INT32 follower;
	SINT8 localplayer = -1;
	UINT16 i;

#ifdef PARANOIA
	if (playernum < 0 || playernum > MAXPLAYERS)
		I_Error("There is no player %d!", playernum);
#endif

	for (i = 0; i <= splitscreen; i++)
	{
		if (playernum == g_localplayers[i])
		{
			snacpending[i]--;

#ifdef PARANOIA
			if (snacpending[i] < 0)
				I_Error("snacpending[%d] negative!", i);
#endif

			localplayer = i;
			break;
		}
	}

	READSTRINGN(*cp, name, MAXPLAYERNAME);
	color = READUINT16(*cp);
	skin = READUINT16(*cp);
	follower = READINT32(*cp);
	followercolor = READUINT16(*cp);
	voice = READUINT16(*cp);

	// set name
	if (player_name_changes[playernum] < MAXNAMECHANGES)
	{
		if (strcasecmp(player_names[playernum], name) != 0)
			SetPlayerName(playernum, name);
	}

	// set color
	p->skincolor = color % numskincolors;
	if (p->mo)
		p->mo->color = (UINT16)p->skincolor;
	demo_extradata[playernum] |= DXD_COLOR;

	// normal player colors
	if (server && !P_IsMachineLocalPlayer(p))
	{
		boolean kick = false;

		// team colors
		if (G_GametypeHasTeams())
		{
			if (p->ctfteam == 1 && p->skincolor != skincolor_redteam)
				kick = true;
			else if (p->ctfteam == 2 && p->skincolor != skincolor_blueteam)
				kick = true;
		}

		// don't allow inaccessible colors
		if (skincolors[p->skincolor].accessible == false)
			kick = true;

		if (kick)
		{
			CONS_Alert(CONS_WARNING, M_GetText("Illegal color change received from %s (team: %d), color: %d)\n"), player_names[playernum], p->ctfteam, p->skincolor);
			SendKick(playernum, KICK_MSG_CON_FAIL);
			return;
		}
	}

	// set skin
	if (cv_forceskin.value >= 0 && K_CanChangeRules(true)) // Server wants everyone to use the same player
	{
		const INT32 forcedskin = cv_forceskin.value;
		SetPlayerSkinByNum(playernum, forcedskin);

		if (localplayer != -1)
		{
			CV_StealthSet(&cv_skin[localplayer], skins[forcedskin].name);
			CV_StealthSet(&cv_voice[localplayer], skins[forcedskin].voices[0].name);
		}

		// set voice
		SetPlayerVoiceByNum(playernum, skins[forcedskin].voices[0].id);
	}
	else
	{
		SetPlayerSkinByNum(playernum, skin);

		// set voice
		SetPlayerVoiceByNum(playernum, voice);

		if (localplayer != -1)
		{
			CV_StealthSet(&cv_voice[localplayer], skins[p->skin].voices[p->voice_id].name);
		}
	}

	// set follower colour:
	// Don't bother doing garbage and kicking if we receive None,
	// this is both silly and a waste of time,
	// this will be handled properly in K_HandleFollower.
	p->followercolor = followercolor;

	// set follower
	K_SetFollowerByNum(playernum, follower);
}

enum {
	WP_KICKSTARTACCEL = 1<<0,
	WP_SHRINKME = 1<<1,
	WP_FLIPCAM = 1<<2,
	WP_LEGACYJITTER = 1<<3,
	WP_RANDOMRESTAT = 1<<4
};

void WeaponPref_Send(UINT8 ssplayer)
{
	UINT8 prefs = 0;

	if (cv_dummyrestatspeed[ssplayer].value > 9 || cv_dummyrestatspeed[ssplayer].value < 0)
		CV_StealthSetValue(&cv_dummyrestatspeed[ssplayer], 0);
	if (cv_dummyrestatweight[ssplayer].value > 9 || cv_dummyrestatweight[ssplayer].value < 0)
		CV_StealthSetValue(&cv_dummyrestatweight[ssplayer], 0);

	if (cv_kickstartaccel[ssplayer].value)
		prefs |= WP_KICKSTARTACCEL;

	if (cv_shrinkme[ssplayer].value)
		prefs |= WP_SHRINKME;

	if (cv_flipcam[ssplayer].value)
		prefs |= WP_FLIPCAM;

	if (!(cv_jitterlegacy[ssplayer].value))
		prefs |= WP_LEGACYJITTER;
	
	if (cv_dummyrestatrandom[ssplayer].value)
		prefs |= WP_RANDOMRESTAT;

	UINT8 buf[5];
	buf[0] = prefs;
	buf[1] = cv_mindelay.value;
	buf[2] = cv_dummyrestatspeed[ssplayer].value;
	buf[3] = cv_dummyrestatweight[ssplayer].value;
	buf[4] = cv_driftmode[ssplayer].value;

	SendNetXCmdForPlayer(ssplayer, XD_WEAPONPREF, buf, sizeof buf);
}

void WeaponPref_Save(UINT8 **cp, INT32 playernum)
{
	player_t *player = &players[playernum];

	UINT8 prefs = 0;

	if (player->pflags & PF_KICKSTARTACCEL)
		prefs |= WP_KICKSTARTACCEL;

	if (player->pflags & PF_SHRINKME)
		prefs |= WP_SHRINKME;

	if (player->pflags & PF_FLIPCAM)
		prefs |= WP_FLIPCAM;

	if (player->jitterlegacy)
		prefs |= WP_LEGACYJITTER;

	if (player->randomrestat)
		prefs |= WP_RANDOMRESTAT;

	WRITEUINT8(*cp, prefs);
}

void WeaponPref_Parse(UINT8 **cp, INT32 playernum)
{
	player_t *player = &players[playernum];

	UINT8 prefs = READUINT8(*cp);

	player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_FLIPCAM);
	player->jitterlegacy = false;
	player->randomrestat = false;

	if (prefs & WP_KICKSTARTACCEL)
		player->pflags |= PF_KICKSTARTACCEL;

	if (prefs & WP_SHRINKME)
		player->pflags |= PF_SHRINKME;

	if (prefs & WP_FLIPCAM)
		player->pflags |= PF_FLIPCAM;

	if (prefs & WP_LEGACYJITTER)
		player->jitterlegacy = true;

	if (prefs & WP_RANDOMRESTAT)
		player->randomrestat = true;

	if (leveltime < 2)
	{
		// BAD HACK: No other place I tried to slot this in
		// made it work for the host when they initally host,
		// so this will have to do.
		K_UpdateShrinkCheat(player);
	}
}

static void Got_WeaponPref(UINT8 **cp,INT32 playernum)
{
	player_t *player = &players[playernum];

	WeaponPref_Parse(cp, playernum);

	UINT8 mindelay = READUINT8(*cp);
	if (server)
	{
		for (UINT8 i = 0; i < G_LocalSplitscreenPartySize(playernum); ++i)
		playerdelaytable[G_LocalSplitscreenPartyMember(playernum, i)] = mindelay;
	}
	UINT8 kartspeedrestat = READUINT8(*cp);
	UINT8 kartweightrestat = READUINT8(*cp);
	if (kartspeedrestat < 0 || kartspeedrestat > 9 ||
		kartweightrestat < 0 || kartweightrestat > 9)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal restat values received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}
	player->kartspeedrestat = kartspeedrestat;
	player->kartweightrestat = kartweightrestat;

	UINT8 driftmode = READUINT8(*cp);
	if (driftmode >= NUMDRIFTMODES)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal drift mode received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}
	player->driftmode = driftmode;

	// SEE ALSO g_demo.c
	demo_extradata[playernum] |= DXD_WEAPONPREF;
}

static void Got_PowerLevel(UINT8 **cp,INT32 playernum)
{
	UINT16 race = (UINT16)READUINT16(*cp);
	UINT16 battle = (UINT16)READUINT16(*cp);

	clientpowerlevels[playernum][PWRLV_RACE] = min(PWRLVRECORD_MAX, race);
	clientpowerlevels[playernum][PWRLV_BATTLE] = min(PWRLVRECORD_MAX, battle);

	CONS_Debug(DBG_GAMELOGIC, "set player %d to power %d\n", playernum, race);
}

static void Got_PartyInvite(UINT8 **cp,INT32 playernum)
{
	UINT8 invitee;

	boolean kick = false;

	invitee = READUINT8 (*cp);

	if (
			invitee < MAXPLAYERS &&
			playeringame[invitee] &&
			playerconsole[playernum] == playernum/* only consoleplayer may! */
	){
		invitee = playerconsole[invitee];
		/* you cannot invite yourself or your computer */
		if (invitee == playernum)
			kick = true;
	}
	else
		kick = true;

	if (kick)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal splitscreen invitation received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	if (splitscreen_invitations[invitee] < 0)
	{
		splitscreen_invitations[invitee] = playernum;

		if (invitee == consoleplayer)/* hey that's me! */
		{
			HU_AddChatText(va(
						"\x82*You have been invited to join %s.",
						VaguePartyDescription(
							playernum, G_PartySize(playernum), '\x82')
			), true);
		}
	}
}

static void Got_AcceptPartyInvite(UINT8 **cp,INT32 playernum)
{
	int invitation;

	(void)cp;

	if (playerconsole[playernum] != playernum)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal accept splitscreen invite received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	invitation = splitscreen_invitations[playernum];

	if (invitation >= 0)
	{
		if (G_IsPartyLocal(invitation))
		{
			HU_AddChatText(va(
						"\x82*%s joined your party!",
						VaguePartyDescription(
							playernum, G_LocalSplitscreenPartySize(playernum), '\x82')
			), true);
		}
		else if (playernum == consoleplayer)
		{
			HU_AddChatText(va(
						"\x82*You joined %s's party!",
						VaguePartyDescription(
							invitation, G_PartySize(invitation), '\x82')
			), true);
		}

		G_JoinParty(invitation, playernum);

		splitscreen_invitations[playernum] = -1;
	}
}

static void Got_CancelPartyInvite(UINT8 **cp,INT32 playernum)
{
	UINT8 invitee;

	invitee = READUINT8 (*cp);

	if (
			invitee < MAXPLAYERS &&
			playeringame[invitee]
	){
		invitee = playerconsole[invitee];

		if (splitscreen_invitations[invitee] == playerconsole[playernum])
		{
			splitscreen_invitations[invitee] = -1;

			if (consoleplayer == invitee)
			{
				HU_AddChatText("\x85*Your invitation has been rescinded.", true);
			}
		}
	}
	else
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal cancel splitscreen invite received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
	}
}

static void Got_LeaveParty(UINT8 **cp,INT32 playernum)
{
	(void)cp;

	if (playerconsole[playernum] != playernum)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal accept splitscreen invite received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	if (consoleplayer == splitscreen_invitations[playernum])
	{
		HU_AddChatText(va(
					"\x85*\x83%s\x85 rejected your invitation.",
					player_names[playernum]
		), true);
	}

	splitscreen_invitations[playernum] = -1;

	if (G_IsPartyLocal(playernum) && playernum != consoleplayer)
	{
		HU_AddChatText(va(
					"\x85*%s left your party.",
					VaguePartyDescription(
						playernum, G_LocalSplitscreenPartySize(playernum), '\x85')
		), true);
	}

	G_LeaveParty(playernum);
}

void D_SendPlayerConfig(UINT8 n)
{
	UINT8 buf[4];
	UINT8 *p = buf;

	SendNameAndColor(n);
	WeaponPref_Send(n);

	if (n == 0)
	{
		// Send it over
		WRITEUINT16(p, vspowerlevel[PWRLV_RACE]);
		WRITEUINT16(p, vspowerlevel[PWRLV_BATTLE]);
	}
	else
	{
		// Splitscreen players have invalid powerlevel
		WRITEUINT16(p, 0);
		WRITEUINT16(p, 0);
	}

	SendNetXCmdForPlayer(n, XD_POWERLEVEL, buf, p-buf);
}

void D_Cheat(INT32 playernum, INT32 cheat, ...)
{
	va_list ap;

	UINT8 buf[64];
	UINT8 *p = buf;

	if (client && !IsPlayerAdmin(consoleplayer))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	if (demo.playback && cheat == CHEAT_DEVMODE)
	{
		// There is no networking in demos, but devmode is
		// too useful to be inaccessible!
		// TODO: maybe allow everything, even though it would
		// desync replays? May be useful for debugging.
		va_start(ap, cheat);
		cht_debug = va_arg(ap, UINT32);
		va_end(ap);
		return;
	}

	// sanity check
	if (demo.playback)
	{
		return;
	}

	WRITEUINT8(p, playernum);
	WRITEUINT8(p, cheat);

	va_start(ap, cheat);
#define COPY(writemacro, type) writemacro (p, va_arg(ap, type))

	switch (cheat)
	{
		case CHEAT_SAVECHECKPOINT:
			COPY(WRITEFIXED, fixed_t); // x
			COPY(WRITEFIXED, fixed_t); // y
			COPY(WRITEFIXED, fixed_t); // z
			break;

		case CHEAT_RINGS:
		case CHEAT_LIVES:
			// If you're confused why 'int' instead of
			// 'SINT8', search online: 'default argument promotions'
			COPY(WRITESINT8, int);
			break;

		case CHEAT_SCALE:
			COPY(WRITEFIXED, fixed_t);
			break;

		case CHEAT_HURT:
			COPY(WRITEINT32, INT32);
			break;

		case CHEAT_RELATIVE_TELEPORT:
		case CHEAT_TELEPORT:
			COPY(WRITEFIXED, fixed_t);
			COPY(WRITEFIXED, fixed_t);
			COPY(WRITEFIXED, fixed_t);
			break;

		case CHEAT_DEVMODE:
			COPY(WRITEUINT32, UINT32);
			break;

		case CHEAT_GIVEITEM:
			COPY(WRITESINT8, int);
			COPY(WRITEUINT8, unsigned int);
			break;

		case CHEAT_SCORE:
			COPY(WRITEUINT32, UINT32);
			break;

		case CHEAT_ANGLE:
			COPY(WRITEANGLE, angle_t);
			break;

	}

#undef COPY
	va_end(ap);

	SendNetXCmd(XD_CHEAT, buf, p - buf);
}

// Only works for displayplayer, sorry!
static void Command_ResetCamera_f(void)
{
	P_ResetCamera(&players[g_localplayers[0]], &camera[0]);
}

/* Consider replacing nametonum with this */
static INT32 LookupPlayer(const char *s)
{
	INT32 playernum;

	if (*s == '0')/* clever way to bypass atoi */
		return 0;

	if (( playernum = atoi(s) ))
	{
		playernum = max(min(playernum, MAXPLAYERS-1), 0);/* not out of range */
		return playernum;
	}

	for (playernum = 0; playernum < MAXPLAYERS; ++playernum)
	{
		/* Match name case-insensitively: fully, or partially the start. */
		if (playeringame[playernum])
			if (strnicmp(player_names[playernum], s, strlen(s)) == 0)
		{
			return playernum;
		}
	}
	return -1;
}

static INT32 FindPlayerByPlace(INT32 place)
{
	INT32 playernum;
	for (playernum = 0; playernum < MAXPLAYERS; ++playernum)
		if (playeringame[playernum])
	{
		if (players[playernum].position == place)
		{
			return playernum;
		}
	}
	return -1;
}

//
// GetViewablePlayerPlaceRange
// Return in first and last, that player available to view, sorted by placement
// in the race.
//
static void GetViewablePlayerPlaceRange(INT32 *first, INT32 *last)
{
	INT32 i;
	INT32 place;

	(*first) = MAXPLAYERS;
	(*last) = 0;

	for (i = 0; i < MAXPLAYERS; ++i)
		if (G_CouldView(i))
	{
		place = players[i].position;
		if (place < (*first))
			(*first) = place;
		if (place > (*last))
			(*last) = place;
	}
}

#define PRINTVIEWPOINT( pre,suf ) \
	CONS_Printf(pre"viewing \x84(%d) \x83%s\x80"suf".\n",\
			(*displayplayerp), player_names[(*displayplayerp)]);
static void Command_View_f(void)
{
	INT32 *displayplayerp;
	INT32 olddisplayplayer;
	int viewnum;
	const char *playerparam;
	INT32 placenum;
	INT32 playernum;
	INT32 firstplace, lastplace;
	char c;
	/* easy peasy */
	c = COM_Argv(0)[strlen(COM_Argv(0))-1];/* may be digit */
	switch (c)
	{
		case '2': viewnum = 2; break;
		case '3': viewnum = 3; break;
		case '4': viewnum = 4; break;
		default:  viewnum = 1;
	}

	if (viewnum > 1 && !( multiplayer && demo.playback ))
	{
		CONS_Alert(CONS_NOTICE,
				"You must be viewing a multiplayer replay to use this.\n");
		return;
	}

	if (camera[viewnum-1].freecam)
		return;

	displayplayerp = &displayplayers[viewnum-1];

	if (COM_Argc() > 1)/* switch to player */
	{
		playerparam = COM_Argv(1);
		if (playerparam[0] == '#')/* search by placement */
		{
			placenum = atoi(&playerparam[1]);
			playernum = FindPlayerByPlace(placenum);
			if (playernum == -1 || !G_CouldView(playernum))
			{
				GetViewablePlayerPlaceRange(&firstplace, &lastplace);
				if (playernum == -1)
				{
					CONS_Alert(CONS_WARNING, "There is no player in that place! ");
				}
				else
				{
					CONS_Alert(CONS_WARNING,
							"That player cannot be viewed currently! "
							"The first player that you can view is \x82#%d\x80; ",
							firstplace);
				}
				CONS_Printf("Last place is \x82#%d\x80.\n", lastplace);
				return;
			}
		}
		else
		{
			if (( playernum = LookupPlayer(COM_Argv(1)) ) == -1)
			{
				CONS_Alert(CONS_WARNING, "There is no player by that name!\n");
				return;
			}
			if (!playeringame[playernum])
			{
				CONS_Alert(CONS_WARNING, "There is no player using that slot!\n");
				return;
			}
		}

		olddisplayplayer = (*displayplayerp);
		G_ResetView(viewnum, playernum, false);

		/* The player we wanted was corrected to who it already was. */
		if ((*displayplayerp) == olddisplayplayer)
			return;

		if ((*displayplayerp) != playernum)/* differ parameter */
		{
			/* skipped some */
			CONS_Alert(CONS_NOTICE, "That player cannot be viewed currently.\n");
			PRINTVIEWPOINT ("Now "," instead")
		}
		else
			PRINTVIEWPOINT ("Now ",)
	}
	else/* print current view */
	{
		if (r_splitscreen < viewnum-1)/* We can't see those guys! */
			return;
		PRINTVIEWPOINT ("Currently ",)
	}
}
#undef PRINTVIEWPOINT

static void Command_SetViews_f(void)
{
	UINT8 splits;
	UINT8 newsplits;

	if (COM_Argc() != 2)
	{
		CONS_Printf("setviews <views>: set the number of split screens\n");
		return;
	}

	splits = r_splitscreen+1;

	newsplits = atoi(COM_Argv(1));
	newsplits = min(max(newsplits, 1), 4);

	if (newsplits > splits && demo.playback && multiplayer)
	{
		G_AdjustView(newsplits, 0, true);
	}
	else
	{
		// Even if the splits go beyond the real number of
		// splitscreen players, displayplayers was filled
		// with duplicates of P1 (see Got_AddPlayer).
		if (demo.playback)
		{
			G_SyncDemoParty(consoleplayer, newsplits-1);
		}
		else
		{
			r_splitscreen = newsplits-1;
			R_ExecuteSetViewSize();
		}

		// If promoting (outside of replays), make sure the
		// camera is in the correct position.
		UINT8 i;
		for (i = splits + 1; i <= newsplits; ++i)
		{
			G_FixCamera(i);
		}
	}
}

static void
Command_Invite_f (void)
{
	UINT8 buffer[1];

	int invitee;

	if (COM_Argc() != 2)
	{
		CONS_Printf("invite <player>: Invite a player to your party.\n");
		return;
	}

	if (G_PartySize(consoleplayer) >= MAXSPLITSCREENPLAYERS)
	{
		CONS_Alert(CONS_WARNING, "Your party is full!\n");
		return;
	}

	invitee = LookupPlayer(COM_Argv(1));

	if (invitee == -1)
	{
		CONS_Alert(CONS_WARNING, "There is no player by that name!\n");
		return;
	}
	if (!playeringame[invitee])
	{
		CONS_Alert(CONS_WARNING, "There is no player using that slot!\n");
		return;
	}

	if (G_IsPartyLocal(invitee))
	{
		CONS_Alert(CONS_WARNING, "That player is already a member of your party.\n");
		return;
	}

	if (splitscreen_invitations[invitee] >= 0)
	{
		CONS_Alert(CONS_WARNING,
				"That player has already been invited to join another party.\n");
		return;
	}

	if ((G_PartySize(consoleplayer) + G_LocalSplitscreenPartySize(invitee)) > MAXSPLITSCREENPLAYERS)
	{
		CONS_Alert(CONS_WARNING,
				"That player joined with too many "
				"splitscreen players for your party.\n");
		return;
	}

	CONS_Printf(
			"Inviting %s...\n",
			VaguePartyDescription(
				invitee, G_LocalSplitscreenPartySize(invitee), '\x80')
	);

	buffer[0] = invitee;

	SendNetXCmd(XD_PARTYINVITE, buffer, sizeof buffer);
}

static void
Command_CancelInvite_f (void)
{
	UINT8 buffer[1];

	int invitee;

	if (COM_Argc() != 2)
	{
		CONS_Printf("cancelinvite <player>: Rescind a party invitation.\n");
		return;
	}

	invitee = LookupPlayer(COM_Argv(1));

	if (invitee == -1)
	{
		CONS_Alert(CONS_WARNING, "There is no player by that name!\n");
		return;
	}
	if (!playeringame[invitee])
	{
		CONS_Alert(CONS_WARNING, "There is no player using that slot!\n");
		return;
	}

	if (splitscreen_invitations[invitee] != consoleplayer)
	{
		CONS_Alert(CONS_WARNING,
				"You have not invited this player!\n");
		return;
	}

	CONS_Printf(
			"Rescinding invite to %s...\n",
			VaguePartyDescription(
				invitee, G_LocalSplitscreenPartySize(invitee), '\x80')
	);

	buffer[0] = invitee;

	SendNetXCmd(XD_CANCELPARTYINVITE, buffer, sizeof buffer);
}

static boolean
CheckPartyInvite (void)
{
	if (splitscreen_invitations[consoleplayer] < 0)
	{
		CONS_Alert(CONS_WARNING, "There is no open party invitation.\n");
		return false;
	}
	return true;
}

static void
Command_AcceptInvite_f (void)
{
	if (CheckPartyInvite())
		SendNetXCmd(XD_ACCEPTPARTYINVITE, NULL, 0);
}

static void
Command_RejectInvite_f (void)
{
	if (CheckPartyInvite())
	{
		CONS_Printf("\x85Rejecting invite...\n");

		SendNetXCmd(XD_LEAVEPARTY, NULL, 0);
	}
}

static void
Command_LeaveParty_f (void)
{
	if (G_PartySize(consoleplayer) > G_LocalSplitscreenPartySize(consoleplayer))
	{
		CONS_Printf("\x85Leaving party...\n");

		SendNetXCmd(XD_LEAVEPARTY, NULL, 0);
	}
}

// ========================================================================

// play a demo, add .lmp for external demos
// eg: playdemo demo1 plays the internal game demo
//
// UINT8 *demofile; // demo file buffer
static void Command_Playdemo_f(void)
{
	char name[256];

	if (COM_Argc() < 2)
	{
		CONS_Printf("playdemo <demoname> [-addfiles / -force]:\n");
		CONS_Printf(M_GetText(
					"Play back a demo file. The full path from your Kart directory must be given.\n\n"

					"* With \"-addfiles\", any required files are added from a list contained within the demo file.\n"
					"* With \"-force\", the demo is played even if the necessary files have not been added.\n"));
		return;
	}

	if (netgame)
	{
		CONS_Printf(M_GetText("You can't play a demo while in a netgame.\n"));
		return;
	}

	// disconnect from server here?
	if (demo.playback)
		G_StopDemo();
	if (metalplayback)
		G_StopMetalDemo();

	// open the demo file
	strcpy(name, COM_Argv(1));
	// dont add .lmp so internal game demos can be played

	CONS_Printf(M_GetText("Playing back demo '%s'.\n"), name);

	demo.loadfiles = fastcmp(COM_Argv(2), "-addfiles");
	demo.ignorefiles = fastcmp(COM_Argv(2), "-force");

	// Internal if no extension, external if one exists
	// If external, convert the file name to a path in SRB2's home directory
	if (FIL_CheckExtension(name))
		G_DoPlayDemo(va("%s"PATHSEP"%s", srb2home, name));
	else
		G_DoPlayDemo(name);
}

static void Command_Timedemo_f(void)
{
	size_t i = 0;

	if (COM_Argc() < 2)
	{
		CONS_Printf(M_GetText("timedemo <demoname> [-csv [<trialid>]] [-quit]: time a demo\n"));
		return;
	}

	if (netgame)
	{
		CONS_Printf(M_GetText("You can't play a demo while in a netgame.\n"));
		return;
	}

	// disconnect from server here?
	if (demo.playback)
		G_StopDemo();
	if (metalplayback)
		G_StopMetalDemo();

	// open the demo file
	strcpy (timedemo_name, COM_Argv(1));
	// dont add .lmp so internal game demos can be played

	// print timedemo results as CSV?
	i = COM_CheckParm("-csv");
	timedemo_csv = (i > 0);
	if (COM_CheckParm("-quit") != i + 1)
		strcpy(timedemo_csv_id, COM_Argv(i + 1)); // user-defined string to identify row
	else
		timedemo_csv_id[0] = 0;

	// exit after the timedemo?
	timedemo_quit = (COM_CheckParm("-quit") > 0);

	CONS_Printf(M_GetText("Timing demo '%s'.\n"), timedemo_name);

	G_TimeDemo(timedemo_name);
}

// stop current demo
static void Command_Stopdemo_f(void)
{
	G_CheckDemoStatus();
	CONS_Printf(M_GetText("Stopped demo.\n"));
}

static void Command_StartMovie_f(void)
{
	M_StartMovie();
}

static void Command_StopMovie_f(void)
{
	M_StopMovie();
}

INT32 mapchangepending = 0;

/** Runs a map change.
  * The supplied data are assumed to be good. If provided by a user, they will
  * have already been checked in Command_Map_f().
  *
  * Do \b NOT call this function directly from a menu! M_Responder() is called
  * from within the event processing loop, and this function calls
  * SV_SpawnServer(), which calls CL_ConnectToServer(), which gives you "Press
  * ESC to abort", which calls I_GetKey(), which adds an event. In other words,
  * 63 old events will get reexecuted, with ridiculous results. Just don't do
  * it (without setting delay to 1, which is the current solution).
  *
  * \param mapnum          Map number to change to.
  * \param gametype        Gametype to switch to.
  * \param pencoremode     Is this 'Encore Mode'?
  * \param resetplayers    1 to reset player scores and lives and such, 0 not to.
  * \param delay           Determines how the function will be executed: 0 to do
  *                        it all right now (must not be done from a menu), 1 to
  *                        do step one and prepare step two, 2 to do step two.
  * \param skipprecutscene To skip the precutscence or not?
  * \sa D_GameTypeChanged, Command_Map_f
  * \author Graue <graue@oceanbase.org>
  */
void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean resetplayers, INT32 delay, boolean skipprecutscene, boolean FLS)
{
	static char buf[2+MAX_WADPATH+1+4];
	static char *buf_p = buf;
	// The supplied data are assumed to be good.
	I_Assert(delay >= 0 && delay <= 2);

	if (mapnum != -1)
	{
		CV_SetValue(&cv_nextmap, mapnum);
	}

	CONS_Debug(DBG_GAMELOGIC, "Map change: mapnum=%d gametype=%d pencoremode=%d resetplayers=%d delay=%d skipprecutscene=%d\n",
	           mapnum, newgametype, pencoremode, resetplayers, delay, skipprecutscene);

	if ((netgame || multiplayer) && (grandprixinfo.gp != false))
		FLS = false;

	// Too lazy to change the input value for every instance of this function.......
	if (bossinfo.boss == true)
	{
		pencoremode = bossinfo.encore;
	}
	else if (specialStage.active == true)
	{
		pencoremode = specialStage.encore;
	}
	else if (grandprixinfo.gp == true)
	{
		pencoremode = grandprixinfo.encore;
	}

	if (delay != 2)
	{
		UINT8 flags = 0;
		//I_Assert(W_CheckNumForName(mapname) != LUMPERROR);
		buf_p = buf;
		if (pencoremode)
			flags |= 1;
		if (!resetplayers)
			flags |= 1<<1;
		if (skipprecutscene)
			flags |= 1<<2;
		if (FLS)
			flags |= 1<<3;
		WRITEUINT8(buf_p, flags);

		// new gametype value
		WRITEUINT8(buf_p, newgametype);

		WRITEINT16(buf_p, mapnum);
	}

	if (delay == 1)
		mapchangepending = 1;
	else
	{
		mapchangepending = 0;
		// spawn the server if needed
		// reset players if there is a new one
		if (!IsPlayerAdmin(consoleplayer))
		{
			if (SV_SpawnServer())
				buf[0] &= ~(1<<1);
			if (!Playing()) // you failed to start a server somehow, so cancel the map change
				return;
		}

		chmappending++;

		if (netgame)
			WRITEUINT32(buf_p, M_RandomizedSeed()); // random seed
		SendNetXCmd(XD_MAP, buf, buf_p - buf);
	}
}

void D_SetupVote(void)
{
	UINT8 buf[13*2]; // twelve UINT16 maps (at twice the width of a UINT8), and two gametypes
	UINT8 *p = buf;
	INT32 i;
	UINT8 gt = (cv_kartgametypepreference.value == -1) ? gametype : cv_kartgametypepreference.value;
	UINT8 secondgt = G_SometimesGetDifferentGametype(gt);
	INT16 votebuffer[4] = {-1,-1,-1,0};

	if (cv_kartencore.value && cv_encorevotes.value == 1 && (gametypes[gt]->rules & GTR_CIRCUIT))
		WRITEUINT8(p, (gt|VOTEMODIFIER_ENCORE));
	else
		WRITEUINT8(p, gt);

	if (cv_kartencore.value && cv_encorevotes.value == 0 && (gametypes[gametype]->rules & GTR_CIRCUIT))
		WRITEUINT8(p, secondgt|VOTEMODIFIER_ENCORE);
	else
		WRITEUINT8(p, secondgt);
	secondgt &= ~VOTEMODIFIER_ENCORE;

	for (i = 0; i < VOTEROWSADDSONE; i++)
	{
		UINT16 m;
		UINT16 hellpick = 0;

		hellpick = ((i == ((VOTEROWS) + 1) ) ? 2 : 0);

		if (i == VOTEROWS)
			hellpick = 1;

		if (i == 2) // sometimes a different gametype
			m = G_RandMap(G_TOLFlag(secondgt), prevmap, ((secondgt != gametype) ? 2 : 0), 0, votebuffer);
		else if (i >= VOTEROWS) // unknown-random and formerly force-unknown MAP HELL
			m = G_RandMap(G_TOLFlag(gt), prevmap, 0, hellpick, votebuffer);
		else
			m = G_RandMap(G_TOLFlag(gt), prevmap, 0, 0, votebuffer);
		if (i < VOTEROWS)
			votebuffer[min(i, 2)] = m; // min() is a dumb workaround for gcc 4.4 array-bounds error
		WRITEUINT16(p, m);
	}

	SendNetXCmd(XD_SETUPVOTE, buf, p - buf);
}

void D_ModifyClientVote(UINT8 player, SINT8 voted)
{
	char buf[3];
	char *p = buf;
	UINT8 sendPlayer = 0;

	if (player >= MAXPLAYERS)
	{
		// Can be used for special game votes later.
		if (!server)
		{
			return;
		}
	}

	WRITEUINT8(p, player);

	if (player <= MAXPLAYERS)
	{
		INT32 i;
		for (i = 0; i <= splitscreen; i++)
		{
			if (g_localplayers[i] == player)
			{
				sendPlayer = i;
			}
		}
	}

	WRITESINT8(p, voted);
	SendNetXCmdForPlayer(sendPlayer, XD_MODIFYVOTE, &buf, p - buf);
}

void D_PickVote(void)
{
	char buf[2];
	char* p = buf;
	SINT8 temppicks[MAXPLAYERS];
	SINT8 templevels[MAXPLAYERS];
	SINT8 votecompare = VOTE_NOT_PICKED;
	UINT8 numvotes = 0, key = 0;
	INT32 i;

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (!Y_PlayerIDCanVote(i))
			continue;
		if (g_votes[i] != VOTE_NOT_PICKED)
		{
			temppicks[numvotes] = i;
			templevels[numvotes] = g_votes[i];
			numvotes++;
			if (votecompare == VOTE_NOT_PICKED)
				votecompare = g_votes[i];
		}
	}

	if (numvotes > 0)
	{
		key = M_RandomKey(numvotes);
		WRITESINT8(p, temppicks[key]);
		WRITESINT8(p, templevels[key]);
	}
	else
	{
		WRITESINT8(p, VOTE_NOT_PICKED);
		WRITESINT8(p, 0);
	}

	SendNetXCmd(XD_PICKVOTE, &buf, 2);
}

static char *ConcatCommandArgv (int start, int end)
{
	char *final;

	size_t size;

	int i;
	char *p;

	size = 0;

	for (i = start; i < end; ++i)
	{
		/*
		one space after each argument, but terminating
		character on final argument
		*/
		size += strlen(COM_Argv(i)) + 1;
	}

	p = final = ZZ_Alloc(size);

	if (!p)
		I_Error("ConcatCommandArgv: Out of memory!\n");

	--end;/* handle the final argument separately */

	for (i = start; i < end; ++i)
	{
		p += sprintf(p, "%s ", COM_Argv(i));
	}

	/* at this point "end" is actually the last argument's position */
	strcpy(p, COM_Argv(end));

	return final;
}

//
// Warp to map code.
// Called either from map <mapname> console command, or idclev cheat.
//
// Largely rewritten by James.
//
static void Command_Map_f(void)
{
	size_t first_option;
	size_t option_force;
	size_t option_gametype;
	size_t option_encore;
	size_t option_skill;
	const char *gametypename;
	boolean newresetplayers;

	boolean mustmodifygame;

	INT32 newmapnum;

	char   *    mapname;
	char   *realmapname = NULL;

	INT32 newgametype = gametype;
	boolean newencoremode = (cv_kartencore.value == 1);

	INT32 d;

	if (client && !IsPlayerAdmin(consoleplayer))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	option_force    =   COM_CheckPartialParm("-f");
	option_gametype =   COM_CheckPartialParm("-g");
	option_encore   =   COM_CheckPartialParm("-e");
	option_skill    =   COM_CheckPartialParm("-s");
	newresetplayers = ! COM_CheckParm("-noresetplayers");

	mustmodifygame = !(netgame || multiplayer) && !majormods;

	if (mustmodifygame)
	{
		/* May want to be more descriptive? */
		CONS_Printf(M_GetText("Sorry, level change disabled in single player.\n"));
		return;
	}

	if (!newresetplayers && !cht_debug)
	{
		CONS_Printf(M_GetText("DEVMODE must be enabled.\n"));
		return;
	}

	if (option_gametype)
	{
		if (!multiplayer)
		{
			CONS_Printf(M_GetText(
						"You can't switch gametypes in single player!\n"));
			return;
		}
		else if (COM_Argc() < option_gametype + 2)/* no argument after? */
		{
			CONS_Alert(CONS_ERROR,
					"No gametype name follows parameter '%s'.\n",
					COM_Argv(option_gametype));
			return;
		}
	}

	if (!( first_option = COM_FirstOption() ))
		first_option = COM_Argc();

	if (first_option < 2)
	{
		/* I'm going over the fucking lines and I DON'T CAREEEEE */
		CONS_Printf("map <name / [MAP]code / number> [-gametype <type>] [-force]:\n");
		CONS_Printf(M_GetText(
					"Warp to a map, by its name, two character code, with optional \"MAP\" prefix, or by its number (though why would you).\n"
					"All parameters are case-insensitive and may be abbreviated.\n"));
		return;
	}

	mapname = ConcatCommandArgv(1, first_option);

	newmapnum = G_FindMapByNameOrCode(mapname, &realmapname);

	if (newmapnum == 0)
	{
		CONS_Alert(CONS_ERROR, M_GetText("Could not find any map described as '%s'.\n"), mapname);
		Z_Free(mapname);
		return;
	}

	if (mustmodifygame && option_force)
	{
		G_SetGameModified(multiplayer, true);
	}

	// new gametype value
	// use current one by default
	if (option_gametype)
	{
		gametypename = COM_Argv(option_gametype + 1);

		newgametype = G_GetGametypeByName(gametypename);

		if (newgametype == -1) // reached end of the list with no match
		{
			/* Did they give us a gametype number? That's okay too! */
			if (isdigit(gametypename[0]))
			{
				d = atoi(gametypename);
				if (d >= 0 && d < numgametypes)
					newgametype = d;
				else
				{
					CONS_Alert(CONS_ERROR,
							"Gametype number %d is out of range. Use a number between"
							" 0 and %d inclusive. ...Or just use the name. :v\n",
							d,
							numgametypes-1);
					Z_Free(realmapname);
					Z_Free(mapname);
					return;
				}
			}
			else
			{
				CONS_Alert(CONS_ERROR,
						"'%s' is not a gametype.\n",
						gametypename);
				Z_Free(realmapname);
				Z_Free(mapname);
				return;
			}
		}
	}
	else if (!Playing())
	{
		newresetplayers = true;
		if (mapheaderinfo[newmapnum-1])
		{
			// Let's just guess so we don't have to specify the gametype EVERY time...
			newgametype = (mapheaderinfo[newmapnum-1]->typeoflevel & TOL_RACE) ? GT_RACE : GT_BATTLE;
		}
	}

	// new encoremode value
	if (option_encore)
	{
		newencoremode = !newencoremode;

		if (!M_SecretUnlocked(SECRET_ENCORE) && newencoremode == true && !option_force)
		{
			CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n"));
			return;
		}
	}

	if (!option_force && newgametype == gametype && Playing()) // SRB2Kart
		newresetplayers = false; // if not forcing and gametypes is the same

	// don't use a gametype the map doesn't support
	if (cht_debug || option_force || cv_skipmapcheck.value)
	{
		fromlevelselect = false; // The player wants us to trek on anyway.  Do so.
	}
	// G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer
	else
	{

		if (!mapheaderinfo[newmapnum-1] || mapheaderinfo[newmapnum-1] == NULL)
		{
			CONS_Alert(CONS_WARNING, M_GetText("Invalid mapheaderinfo for Course %s (%s)\n"), realmapname, G_BuildMapName(newmapnum));
			Z_Free(realmapname);
			Z_Free(mapname);
			return;
		}

		if (!(
					mapheaderinfo[newmapnum-1] &&
					mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)
		))
		{
			CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum), (gametype_cons_t[newgametype].strvalue));
			Z_Free(realmapname);
			Z_Free(mapname);
			return;
		}
		else
		{
			fromlevelselect =
				( netgame || multiplayer ) &&
				grandprixinfo.gp != false;
		}
	}

	if (!(netgame || multiplayer))
	{
		if (newgametype == GT_BATTLE)
		{
			grandprixinfo.gp = false;
			specialStage.active = false;
			K_ResetBossInfo();

			if (mapheaderinfo[newmapnum-1] &&
				mapheaderinfo[newmapnum-1]->typeoflevel & TOL_BOSS)
			{
				bossinfo.boss = true;
				bossinfo.encore = newencoremode;
			}
		}
		else
		{
			if (mapheaderinfo[newmapnum-1] &&
				mapheaderinfo[newmapnum-1]->typeoflevel & TOL_SPECIAL) // Special Stage
			{
				grandprixinfo.gp = false;
				bossinfo.boss = false;

				specialStage.active = true;
				specialStage.encore = newencoremode;
			}
			else // default GP
			{
				grandprixinfo.gamespeed = (cv_kartspeed.value == KARTSPEED_AUTO ? KARTSPEED_DEFAULT : cv_kartspeed.value);
				grandprixinfo.masterbots = false;
				grandprixinfo.lunaticmode = false;

				if (option_skill)
				{
					const char *skillname = COM_Argv(option_skill + 1);
					INT32 newskill = -1;
					INT32 j;

					for (j = 0; gpdifficulty_cons_t[j].strvalue; j++)
					{
						if (!strcasecmp(gpdifficulty_cons_t[j].strvalue, skillname))
						{
							newskill = (INT16)gpdifficulty_cons_t[j].value;
							break;
						}
					}

					if (!gpdifficulty_cons_t[j].strvalue) // reached end of the list with no match
					{
						INT32 num = atoi(COM_Argv(option_skill + 1)); // assume they gave us a skill number, which is okay too
						if (num >= KARTSPEED_EASY && num <= KARTGP_MANIAC)
							newskill = (INT16)num;
					}

					if (newskill != -1)
					{
						if (newskill == KARTGP_MASTER)
						{
							grandprixinfo.gamespeed = KARTSPEED_HARD;
							grandprixinfo.masterbots = true;
						}
						else if (newskill == KARTGP_NIGHTMARE)
						{
							grandprixinfo.gamespeed = KARTSPEED_EXPERT;
							grandprixinfo.masterbots = true;
						}
						else if (newskill == KARTGP_LUNATIC)
						{
							grandprixinfo.gamespeed = KARTSPEED_HARD;
							grandprixinfo.masterbots = true;
							grandprixinfo.lunaticmode = true;
						}
						else if (newskill == KARTGP_MANIAC)
						{
							grandprixinfo.gamespeed = KARTSPEED_EXPERT;
							grandprixinfo.masterbots = true;
							grandprixinfo.lunaticmode = true;
						}
						else
						{
							grandprixinfo.gamespeed = newskill;
							grandprixinfo.masterbots = false;
						}
					}
				}

				grandprixinfo.encore = newencoremode;

				grandprixinfo.gp = true;
				grandprixinfo.roundnum = 0;
				grandprixinfo.cup = NULL;
				grandprixinfo.wonround = false;

				bossinfo.boss = false;
				specialStage.active = false;

				grandprixinfo.initalize = true;
			}
		}
	}

	// Prevent warping to locked levels
	// ... unless you're in a dedicated server.  Yes, technically this means you can view any level by
	// running a dedicated server and joining it yourself, but that's better than making dedicated server's
	// lives hell.
	if (!dedicated && M_MapLocked(newmapnum))
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You need to unlock this level before you can warp to it!\n"));
		Z_Free(realmapname);
		Z_Free(mapname);
		return;
	}

	tutorialmode = false; // warping takes us out of tutorial mode

	D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, fromlevelselect);

	Z_Free(realmapname);
}

/** Receives a map command and changes the map.
  *
  * \param cp        Data buffer.
  * \param playernum Player number responsible for the message. Should be
  *                  ::serverplayer or ::adminplayer.
  * \sa D_MapChange
  */
static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
{
	UINT8 flags;
	INT32 resetplayer = 1, lastgametype;
	UINT8 skipprecutscene, FLS;
	boolean pencoremode;
	INT16 mapnumber;

	forceresetplayers = deferencoremode = false;

	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal map change received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	if (chmappending)
		chmappending--;

	flags = READUINT8(*cp);

	pencoremode = ((flags & 1) != 0);

	resetplayer = ((flags & (1<<1)) == 0);

	lastgametype = gametype;
	gametype = READUINT8(*cp);
	G_SetGametype(gametype); // I fear putting that macro as an argument

	if (gametype < 0 || gametype >= numgametypes)
		gametype = lastgametype;
	else if (gametype != lastgametype)
		D_GameTypeChanged(lastgametype); // emulate consvar_t behavior for gametype

	if (!(gametypes[gametype]->rules & GTR_ENCORE) && !bossinfo.boss)
		pencoremode = false;

	skipprecutscene = ((flags & (1<<2)) != 0);

	FLS = ((flags & (1<<3)) != 0);

	mapnumber = READINT16(*cp);

	if (grandprixinfo.gp)
	{
		if (gametype != GT_RACE)
		{
			grandprixinfo.eventmode = GPEVENT_CHALLENGE;
		}
		else
		{
			grandprixinfo.eventmode = GPEVENT_NONE;
		}
	}

	if (netgame)
		P_SetRandSeed(READUINT32(*cp));

	if (!skipprecutscene)
	{
		DEBFILE(va("Warping to %s [resetplayer=%d lastgametype=%d gametype=%d cpnd=%d]\n",
			G_BuildMapName(mapnumber), resetplayer, lastgametype, gametype, chmappending));
		CON_LogMessage(M_GetText("Speeding off to level...\n"));
	}

	if (resetplayer && !FLS)
	{
		emeralds = 0;
		memset(&luabanks, 0, sizeof(luabanks));
	}

	demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING;
	demo.savebutton = 0;

	// clear this demo before recording a new one
	if (demo.recording && !modeattacking)
		G_CheckDemoStatus();

	G_InitNew(pencoremode, mapnumber, resetplayer, skipprecutscene, FLS);

	if (demo.timing)
		G_DoneLevelLoad();

	if (metalrecording)
		G_BeginMetal();
	if (demo.recording) // Okay, level loaded, character spawned and skinned,
		G_BeginRecording(); // I AM NOW READY TO RECORD.
	demo.deferstart = true;

#ifdef HAVE_DISCORDRPC
	DRPC_UpdatePresence();
#endif
}

static void Command_RandomMap(void)
{
	INT32 oldmapnum;
	INT32 newmapnum;
	INT32 newgametype;
	boolean newencoremode;
	boolean newresetplayers;

	if (client && !IsPlayerAdmin(consoleplayer))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	// TODO: Handle singleplayer conditions.
	// The existing ones are way too annoyingly complicated and "anti-cheat" for my tastes.

	if (Playing())
	{
		newgametype = gametype;
		newencoremode = encoremode;
		newresetplayers = false;

		if (gamestate == GS_LEVEL)
		{
			oldmapnum = gamemap-1;
		}
		else
		{
			oldmapnum = prevmap;
		}
	}
	else
	{
		newgametype = cv_newgametype.value;
		newencoremode = false;
		newresetplayers = true;
		oldmapnum = -1;
	}

	newmapnum = G_RandMap(G_TOLFlag(newgametype), oldmapnum, 0, 0, NULL) + 1;
	D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, false);
}

static void Command_RestartLevel(void)
{
	if (client && !IsPlayerAdmin(consoleplayer))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	if (!Playing())
	{
		CONS_Printf(M_GetText("You must be in a game to use this.\n"));
		return;
	}

	D_MapChange(gamemap, gametype, encoremode, false, 0, false, false);
}

static void Command_Pause(void)
{
	UINT8 buf[2];
	UINT8 *cp = buf;

	if (COM_Argc() > 1)
		WRITEUINT8(cp, (char)(atoi(COM_Argv(1)) != 0));
	else
		WRITEUINT8(cp, (char)(!paused));

	if (dedicated)
		WRITEUINT8(cp, 1);
	else
		WRITEUINT8(cp, 0);

	if (cv_pause.value || server || (IsPlayerAdmin(consoleplayer)))
	{
		if (!paused && (!(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING || gamestate == GS_WAITINGPLAYERS)))
		{
			CONS_Printf(M_GetText("You can't pause here.\n"));
			return;
		}
		else if (modeattacking)	// in time attack, pausing restarts the map
		{
			MR_ModeAttackRetry(0); // directly call from m_menu;
			return;
		}

		SendNetXCmd(XD_PAUSE, &buf, 2);
	}
	else
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
}

static void Got_Pause(UINT8 **cp, INT32 playernum)
{
	UINT8 dedicatedpause = false;
	const char *playername;

	if (netgame && !cv_pause.value && playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal pause command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	if (modeattacking && !demo.playback)
		return;

	paused = READUINT8(*cp);
	dedicatedpause = READUINT8(*cp);

	if (!demo.playback)
	{
		if (netgame)
		{
			if (dedicatedpause)
				playername = "SERVER";
			else
				playername = player_names[playernum];

			if (paused)
				CONS_Printf(M_GetText("Game paused by %s\n"), playername);
			else
				CONS_Printf(M_GetText("Game unpaused by %s\n"), playername);
		}

		if (paused)
		{
			if (!menustack[0] || netgame)
				S_PauseAudio();
		}
		else
			S_ResumeAudio();
	}

	I_UpdateMouseGrab();
	G_ResetAllControllerRumbles();
}

static void Command_ReplayMarker(void)
{
	if (gamestate != GS_LEVEL)
	{
		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
		return;
	}
	if (demo.savemode == DSM_WILLAUTOSAVE)
	{
		demo.savemode = DSM_NOTSAVING;
		CONS_Printf("Replay unmarked.\n");
	}
	else
	{
		demo.savemode = DSM_WILLAUTOSAVE;

		int adjustedleveltime = leveltime - starttime;

		if (adjustedleveltime < 0)
			adjustedleveltime = 0;

		char *title = G_BuildMapTitle(gamemap);

		if (title)
		{
			snprintf(demo.titlename, 64, "%s [%i:%02d/%.5s]", title, G_TicsToMinutes(adjustedleveltime, false), G_TicsToSeconds(adjustedleveltime), modeattacking ? "Record Attack" : connectedservername);
			Z_Free(title);
		}
		else
			snprintf(demo.titlename, 64, "[%i:%02d/%.5s]", G_TicsToMinutes(adjustedleveltime, false), G_TicsToSeconds(adjustedleveltime), modeattacking ? "Record Attack" : connectedservername);

		CONS_Printf("Replay will be saved!\n");
	}
}

// Command for stuck characters in netgames, griefing, etc.
static void HandleRespawnCommand(UINT8 localplayer)
{
	UINT8 buf[1];
	UINT8 *cp = buf;

	if (!(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING))
	{
		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
		return;
	}

	if (players[g_localplayers[localplayer]].mo && !P_IsObjectOnGround(players[g_localplayers[localplayer]].mo)) // KART: Nice try, but no, you won't be cheesing spb anymore.
	{
		CONS_Printf(M_GetText("You must be on the floor to use this.\n"));
		return;
	}

	if (players[g_localplayers[localplayer]].mo && (P_PlayerInPain(&players[g_localplayers[localplayer]]) || spbplace == players[g_localplayers[localplayer]].position)) // KART: Nice try, but no, you won't be cheesing spb anymore (x2)
	{
		CONS_Printf(M_GetText("Nice try.\n"));
		return;
	}

	if (localplayer != 0 && !g_localplayers[localplayer])
	{
		CONS_Printf(M_GetText("There is no player to respawn.\n"));
		return;
	}

	WRITEUINT8(cp, g_localplayers[localplayer]);
	SendNetXCmdForPlayer(localplayer, XD_RESPAWN, &buf, sizeof(buf));
}

// Command for stuck characters in netgames, griefing, etc.
static void Command_Respawn(void)
{
	HandleRespawnCommand(0);
}

static void Command_Respawn2(void)
{
	HandleRespawnCommand(1);
}

static void Command_Respawn3(void)
{
	HandleRespawnCommand(2);
}

static void Command_Respawn4(void)
{
	HandleRespawnCommand(3);
}

static void Got_Respawn(UINT8 **cp, INT32 playernum)
{
	INT32 respawnplayer = READUINT8(*cp);

	// You can't respawn someone else. Nice try, there.
	// Unless it's a bot. They can respawn whenever they feel like it, if the server says so.
	if (players[respawnplayer].bot
	  ? playernum != serverplayer
	  : respawnplayer != playernum || P_PlayerInPain(&players[respawnplayer]) || spbplace == players[respawnplayer].position)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal respawn command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	if (players[respawnplayer].mo)
	{
		// incase the above checks were modified to allow sending a respawn on these occasions:
		if (!P_IsObjectOnGround(players[respawnplayer].mo))
			return;

		P_DamageMobj(players[respawnplayer].mo, NULL, NULL, 1, DMG_INSTAKILL);
		demo_extradata[respawnplayer] |= DXD_RESPAWN;
	}
}

/** Deals with an ::XD_RANDOMSEED message in a netgame.
  * These messages set the position of the random number LUT and are crucial to
  * correct synchronization.
  *
  * Such a message should only ever come from the ::serverplayer. If it comes
  * from any other player, it is ignored.
  *
  * \param cp        Data buffer.
  * \param playernum Player responsible for the message. Must be ::serverplayer.
  * \author Graue <graue@oceanbase.org>
  */
static void Got_RandomSeed(UINT8 **cp, INT32 playernum)
{
	UINT32 seed;

	seed = READUINT32(*cp);

	if (playernum != serverplayer) // it's not from the server, wtf?
		return;

	P_SetRandSeed(seed);
}

/** Clears all players' scores in a netgame.
  * Only the server or a remote admin can use this command, for obvious reasons.
  *
  * \sa XD_CLEARSCORES, Got_Clearscores
  * \author SSNTails <http://www.ssntails.org>
  */
static void Command_Clearscores_f(void)
{
	if (!(server || (IsPlayerAdmin(consoleplayer))))
		return;

	SendNetXCmd(XD_CLEARSCORES, NULL, 1);
}

/** Handles an ::XD_CLEARSCORES message, which resets all players' scores in a
  * netgame to zero.
  *
  * \param cp        Data buffer.
  * \param playernum Player responsible for the message. Must be ::serverplayer
  *                  or ::adminplayer.
  * \sa XD_CLEARSCORES, Command_Clearscores_f
  * \author SSNTails <http://www.ssntails.org>
  */
static void Got_Clearscores(UINT8 **cp, INT32 playernum)
{
	INT32 i;

	(void)cp;
	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal clear scores command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	for (i = 0; i < MAXPLAYERS; i++)
		players[i].score = 0;

	CONS_Printf(M_GetText("Scores have been reset by the server.\n"));
}

static void Command_ScoreboardAdd(void)
{
	if (!server)
	{
		CONS_Printf(M_GetText("Only the server can use this.\n"));
		return;
	}

	if (COM_Argc() < 2)
	{
		CONS_Printf(M_GetText("addscoreboard <text>: Add text to the scoreboard\n"));
		return;
	}

	{
		size_t i, j = COM_Argc();
		char message[MAXSERVERDESCRIPTIONLINE+1];
		char *finalstring;
		INT32 strlensize = 0;
		INT32 serverdesclen = 0;

		// Steal from the motd code so you don't have to put the string in quotes.
		strlcpy(message, COM_Argv(1), sizeof message);
		for (i = 2; i < j; i++)
		{
			strlcat(message, " ", sizeof message);
			strlcat(message, COM_Argv(i), sizeof message);
		}

		strlensize = strlen(message);
		message[strlensize+1] = '\0';
		message[MAXSERVERDESCRIPTIONLINE] = '\0';

		finalstring = Z_Calloc(MAXSERVERDESCRIPTIONLINE*sizeof(char), PU_STATIC, NULL);

		CopyCaretColors(finalstring, message, MAXSERVERDESCRIPTIONLINE);

		strlensize = strlen(finalstring);
		serverdesclen = strlen(connectedserverdescription);

		if ((strlensize+1) > MAXSERVERDESCRIPTIONLINE)
		{
			CONS_Alert(CONS_WARNING, M_GetText("Scoreboard line is too long to add.\n"));
			Z_Free(finalstring);
			return;
		}

		if ((serverdesclen + strlensize) > MAXSERVERDESCRIPTION)
		{
			CONS_Alert(CONS_WARNING, M_GetText("Scoreboard text is full so no more text can be added.\n"));
			Z_Free(finalstring);
			return;
		}
		else
		{
			strncat(connectedserverdescription, va("%s\n", finalstring), MAXSERVERDESCRIPTIONLINE);
			CONS_Printf(M_GetText("Scoreboard text line has been added.\n"));
		}

		ServerInfoUpdateSend();
		Z_Free(finalstring);
	}
}

static void Command_ScoreboardClear(void)
{
	if (!server)
	{
		CONS_Printf(M_GetText("Only the server can use this.\n"));
		return;
	}

	memset(&connectedserverdescription, 0, sizeof(connectedserverdescription));
	ServerInfoUpdateSend();
	CONS_Printf(M_GetText("Scoreboard text have been reset by the server.\n"));
}

// Team changing functions
static void HandleTeamChangeCommand(UINT8 localplayer)
{
	const char *commandname = NULL;
	changeteam_union NetPacket;
	boolean error = false;
	UINT16 usvalue;
	NetPacket.value.l = NetPacket.value.b = 0;

	switch (localplayer)
	{
		case 0:
			commandname = "changeteam";
			break;
		default:
			commandname = va("changeteam%d", localplayer+1);
			break;
	}

	//      0         1
	// changeteam  <color>

	if (COM_Argc() <= 1)
	{
		if (G_GametypeHasTeams())
			CONS_Printf(M_GetText("%s <team>: switch to a new team (%s)\n"), commandname, "red, blue or spectator");
		else if (G_GametypeHasSpectators())
			CONS_Printf(M_GetText("%s <team>: switch to a new team (%s)\n"), commandname, "spectator or playing");
		else
			CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used in this gametype.\n"));
		return;
	}

	if (G_GametypeHasTeams())
	{
		if (!strcasecmp(COM_Argv(1), "red") || !strcasecmp(COM_Argv(1), "1"))
			NetPacket.packet.newteam = 1;
		else if (!strcasecmp(COM_Argv(1), "blue") || !strcasecmp(COM_Argv(1), "2"))
			NetPacket.packet.newteam = 2;
		else if (!strcasecmp(COM_Argv(1), "spectator") || !strcasecmp(COM_Argv(1), "0"))
			NetPacket.packet.newteam = 0;
		else
			error = true;
	}
	else if (G_GametypeHasSpectators())
	{
		if (!strcasecmp(COM_Argv(1), "spectator") || !strcasecmp(COM_Argv(1), "0"))
			NetPacket.packet.newteam = 0;
		else if (!strcasecmp(COM_Argv(1), "playing") || !strcasecmp(COM_Argv(1), "1"))
			NetPacket.packet.newteam = 3;
		else
			error = true;
	}
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used in this gametype.\n"));
		return;
	}

	if (error)
	{
		if (G_GametypeHasTeams())
			CONS_Printf(M_GetText("%s <team>: switch to a new team (%s)\n"), commandname, "red, blue or spectator");
		else if (G_GametypeHasSpectators())
			CONS_Printf(M_GetText("%s <team>: switch to a new team (%s)\n"), commandname, "spectator or playing");
		return;
	}

	if (players[g_localplayers[localplayer]].spectator)
		error = !(NetPacket.packet.newteam || (players[g_localplayers[localplayer]].pflags & PF_WANTSTOJOIN)); // :lancer:
	else if (G_GametypeHasTeams())
		error = (NetPacket.packet.newteam == players[g_localplayers[localplayer]].ctfteam);
	else if (G_GametypeHasSpectators() && !players[g_localplayers[localplayer]].spectator)
		error = (NetPacket.packet.newteam == 3);
#ifdef PARANOIA
	else
		I_Error("Invalid gametype after initial checks!");
#endif

	if (error)
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You're already on that team!\n"));
		return;
	}

	if (!cv_allowteamchange.value && NetPacket.packet.newteam) // allow swapping to spectator even in locked teams.
	{
		CONS_Alert(CONS_NOTICE, M_GetText("The server is not allowing team changes at the moment.\n"));
		return;
	}

	usvalue = SHORT(NetPacket.value.l|NetPacket.value.b);
	SendNetXCmdForPlayer(localplayer, XD_TEAMCHANGE, &usvalue, sizeof(usvalue));
}

static void Command_Teamchange_f(void)
{
	HandleTeamChangeCommand(0);
}

static void Command_Teamchange2_f(void)
{
	HandleTeamChangeCommand(1);
}

static void Command_Teamchange3_f(void)
{
	HandleTeamChangeCommand(2);
}

static void Command_Teamchange4_f(void)
{
	HandleTeamChangeCommand(3);
}

static void Command_ServerTeamChange_f(void)
{
	changeteam_union NetPacket;
	boolean error = false;
	UINT16 usvalue;
	NetPacket.value.l = NetPacket.value.b = 0;

	if (!(server || (IsPlayerAdmin(consoleplayer))))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	//        0              1         2
	// serverchangeteam <playernum>  <team>

	if (COM_Argc() < 3)
	{
		if (G_GametypeHasTeams())
			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "red, blue or spectator");
		else if (G_GametypeHasSpectators())
			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "spectator or playing");
		else
			CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used in this gametype.\n"));
		return;
	}

	if (G_GametypeHasTeams())
	{
		if (!strcasecmp(COM_Argv(2), "red") || !strcasecmp(COM_Argv(2), "1"))
			NetPacket.packet.newteam = 1;
		else if (!strcasecmp(COM_Argv(2), "blue") || !strcasecmp(COM_Argv(2), "2"))
			NetPacket.packet.newteam = 2;
		else if (!strcasecmp(COM_Argv(2), "spectator") || !strcasecmp(COM_Argv(2), "0"))
			NetPacket.packet.newteam = 0;
		else
			error = true;
	}
	else if (G_GametypeHasSpectators())
	{
		if (!strcasecmp(COM_Argv(2), "spectator") || !strcasecmp(COM_Argv(2), "0"))
			NetPacket.packet.newteam = 0;
		else if (!strcasecmp(COM_Argv(2), "playing") || !strcasecmp(COM_Argv(2), "1"))
			NetPacket.packet.newteam = 3;
		else
			error = true;
	}
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used in this gametype.\n"));
		return;
	}

	if (error)
	{
		if (G_GametypeHasTeams())
			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "red, blue or spectator");
		else if (G_GametypeHasSpectators())
			CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "spectator or playing");
		return;
	}

	NetPacket.packet.playernum = atoi(COM_Argv(1));

	if (!playeringame[NetPacket.packet.playernum])
	{
		CONS_Alert(CONS_NOTICE, M_GetText("There is no player %d!\n"), NetPacket.packet.playernum);
		return;
	}

	if (G_GametypeHasTeams())
	{
		if (NetPacket.packet.newteam == players[NetPacket.packet.playernum].ctfteam ||
			(players[NetPacket.packet.playernum].spectator && !NetPacket.packet.newteam))
			error = true;
	}
	else if (G_GametypeHasSpectators())
	{
		if ((players[NetPacket.packet.playernum].spectator && !NetPacket.packet.newteam) ||
			(!players[NetPacket.packet.playernum].spectator && NetPacket.packet.newteam == 3))
			error = true;
	}
#ifdef PARANOIA
	else
		I_Error("Invalid gametype after initial checks!");
#endif

	if (error)
	{
		CONS_Alert(CONS_NOTICE, M_GetText("That player is already on that team!\n"));
		return;
	}

	NetPacket.packet.verification = true; // This signals that it's a server change

	usvalue = SHORT(NetPacket.value.l|NetPacket.value.b);
	SendNetXCmd(XD_TEAMCHANGE, &usvalue, sizeof(usvalue));
}

void P_SetPlayerSpectator(INT32 playernum)
{
	//Make sure you're in the right gametype.
	if (!G_GametypeHasTeams() && !G_GametypeHasSpectators())
		return;

	// Don't duplicate efforts.
	if (players[playernum].spectator)
		return;

	players[playernum].spectator = true;
	players[playernum].pflags &= ~PF_WANTSTOJOIN;

	players[playernum].playerstate = PST_REBORN;
}

//todo: This and the other teamchange functions are getting too long and messy. Needs cleaning.
static void Got_Teamchange(UINT8 **cp, INT32 playernum)
{
	changeteam_union NetPacket;
	boolean error = false, wasspectator = false;
	NetPacket.value.l = NetPacket.value.b = READINT16(*cp);

	if (!G_GametypeHasTeams() && !G_GametypeHasSpectators()) //Make sure you're in the right gametype.
	{
		// this should never happen unless the client is hacked/buggy
		CONS_Alert(CONS_WARNING, M_GetText("Illegal team change received from player %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
	}

	if (NetPacket.packet.verification) // Special marker that the server sent the request
	{
		if (playernum != serverplayer && (!IsPlayerAdmin(playernum)))
		{
			CONS_Alert(CONS_WARNING, M_GetText("Illegal team change received from player %s\n"), player_names[playernum]);
			if (server)
				SendKick(playernum, KICK_MSG_CON_FAIL);
			return;
		}
		playernum = NetPacket.packet.playernum;
	}

	// Prevent multiple changes in one go.
	if (players[playernum].spectator && !(players[playernum].pflags & PF_WANTSTOJOIN) && !NetPacket.packet.newteam)
		return;
	else if (G_GametypeHasTeams())
	{
		if (NetPacket.packet.newteam && (NetPacket.packet.newteam == (unsigned)players[playernum].ctfteam))
			return;
	}
	else if (G_GametypeHasSpectators())
	{
		if (!players[playernum].spectator && NetPacket.packet.newteam == 3)
			return;
	}
	else
	{
		if (playernum != serverplayer && (!IsPlayerAdmin(playernum)))
		{
			CONS_Alert(CONS_WARNING, M_GetText("Illegal team change received from player %s\n"), player_names[playernum]);
			if (server)
				SendKick(playernum, KICK_MSG_CON_FAIL);
		}
		return;
	}

	// Don't switch team, just go away, please, go awaayyyy, aaauuauugghhhghgh
	if (!LUA_HookTeamSwitch(&players[playernum], NetPacket.packet.newteam, players[playernum].spectator, NetPacket.packet.autobalance, NetPacket.packet.scrambled))
		return;

	//Make sure that the right team number is sent. Keep in mind that normal clients cannot change to certain teams in certain gametypes.
#ifdef PARANOIA
	if (!G_GametypeHasTeams() && !G_GametypeHasSpectators())
		I_Error("Invalid gametype after initial checks!");
#endif

	if (!cv_allowteamchange.value)
	{
		if (!NetPacket.packet.verification && NetPacket.packet.newteam)
			error = true; //Only admin can change status, unless changing to spectator.
	}

	if (server && ((NetPacket.packet.newteam < 0 || NetPacket.packet.newteam > 3) || error))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal team change received from player %s\n"), player_names[playernum]);
		SendKick(playernum, KICK_MSG_CON_FAIL);
	}

	//Safety first!
	// (not respawning spectators here...)
	wasspectator = (players[playernum].spectator == true);

	if (!wasspectator)
	{
		if (gamestate == GS_LEVEL && players[playernum].mo)
		{
			// The following will call P_SetPlayerSpectator if successful
			P_DamageMobj(players[playernum].mo, NULL, NULL, 1, DMG_SPECTATOR);
		}

		//...but because the above could return early under some contexts, we try again here
		P_SetPlayerSpectator(playernum);
	}

	//Now that we've done our error checking and killed the player
	//if necessary, put the player on the correct team/status.
	if (G_GametypeHasTeams())
	{
		if (!NetPacket.packet.newteam)
		{
			players[playernum].ctfteam = 0;
			players[playernum].spectator = true;
		}
		else
		{
			players[playernum].ctfteam = NetPacket.packet.newteam;
			players[playernum].pflags |= PF_WANTSTOJOIN; //players[playernum].spectator = false;
		}
	}
	else if (G_GametypeHasSpectators())
	{
		if (!NetPacket.packet.newteam)
			players[playernum].spectator = true;
		else
			players[playernum].pflags |= PF_WANTSTOJOIN; //players[playernum].spectator = false;
	}

	if (NetPacket.packet.autobalance)
	{
		if (NetPacket.packet.newteam == 1)
			CONS_Printf(M_GetText("%s was autobalanced to the %c%s%c.\n"), player_names[playernum], '\x85', M_GetText("Red Team"), '\x80');
		else if (NetPacket.packet.newteam == 2)
			CONS_Printf(M_GetText("%s was autobalanced to the %c%s%c.\n"), player_names[playernum], '\x84', M_GetText("Blue Team"), '\x80');
	}
	else if (NetPacket.packet.scrambled)
	{
		if (NetPacket.packet.newteam == 1)
			CONS_Printf(M_GetText("%s was scrambled to the %c%s%c.\n"), player_names[playernum], '\x85', M_GetText("Red Team"), '\x80');
		else if (NetPacket.packet.newteam == 2)
			CONS_Printf(M_GetText("%s was scrambled to the %c%s%c.\n"), player_names[playernum], '\x84', M_GetText("Blue Team"), '\x80');
	}
	else if (NetPacket.packet.newteam == 1)
	{
		CONS_Printf(M_GetText("%s switched to the %c%s%c.\n"), player_names[playernum], '\x85', M_GetText("Red Team"), '\x80');
	}
	else if (NetPacket.packet.newteam == 2)
	{
		CONS_Printf(M_GetText("%s switched to the %c%s%c.\n"), player_names[playernum], '\x84', M_GetText("Blue Team"), '\x80');
	}
	else if (NetPacket.packet.newteam == 0 && !wasspectator)
		HU_AddChatText(va("\x82*%s became a spectator.", player_names[playernum]), false); // "entered the game" text was moved to P_SpectatorJoinGame

	/*if (G_GametypeHasTeams())
	{
		if (NetPacket.packet.newteam)
		{
			UINT8 i;
			for (i = 0; i <= splitscreen; i++)
			{
				if (playernum == g_localplayers[i]) //CTF and Team Match colors.
					CV_SetValue(&cv_playercolor[i], NetPacket.packet.newteam + 5); - -this calculation is totally wrong
			}
		}
	}*/

	if (gamestate != GS_LEVEL)
		return;

	demo_extradata[playernum] |= DXD_PLAYSTATE;

	// Clear player score and rings if a spectator.
	if (players[playernum].spectator && !wasspectator)
	{
		players[playernum].spectatorreentry = (cv_spectatorreentry.value * TICRATE);

		if (gametypes[gametype]->rules & GTR_BUMPERS) // SRB2kart
		{
			players[playernum].roundscore = 0;
			if (K_IsPlayerWanted(&players[playernum]))
				K_CalculateBattleWanted();
		}

		K_PlayerForfeit(playernum, true);

		if (players[playernum].mo)
			players[playernum].mo->health = 1;

		K_StripItems(&players[playernum]);
	}

	K_CheckBumpers(); // SRB2Kart
	P_CheckRacers(); // also SRB2Kart
}

//
// Attempts to make password system a little sane without
// rewriting the entire goddamn XD_file system
//
#define BASESALT "basepasswordstorage"

void D_SetPassword(const char *pw)
{
	adminpass = Z_Realloc(adminpass, sizeof(*adminpass) * ++adminpasscount, PU_STATIC, NULL);
	adminpass[adminpasscount-1] = Z_StrDup(pw);
}

void D_ClearPassword(void)
{
	UINT32 i;
	for (i = 0; i < adminpasscount; i++)
		Z_Free(adminpass[i]);
	Z_Free(adminpass);
	adminpass = NULL;
	adminpasscount = 0;
}

// Remote Administration
static void Command_Changepassword_f(void)
{
	if (client) // cannot change remotely
	{
		CONS_Printf(M_GetText("Only the server can use this.\n"));
		return;
	}

	if (COM_Argc() != 2)
	{
		CONS_Printf(M_GetText("password <password>: add remote admin password\n"));
		return;
	}

	D_SetPassword(COM_Argv(1));
	CONS_Printf(M_GetText("Password added.\n"));
}


// Remote Administration
static void Command_Clearpassword_f(void)
{
	if (client) // cannot change remotely
	{
		CONS_Printf(M_GetText("Only the server can use this.\n"));
		return;
	}

	D_ClearPassword();
	CONS_Printf(M_GetText("Passwords cleared.\n"));
}

static void Command_Login_f(void)
{
	doomdata_t *netbuffer = DOOMCOM_DATA(doomcom);

	const char *pw;

	if (!netgame)
	{
		CONS_Printf(M_GetText("This only works in a netgame.\n"));
		return;
	}

	// If the server uses login, it will effectively just remove admin privileges
	// from whoever has them. This is good.
	if (COM_Argc() != 2)
	{
		CONS_Printf(M_GetText("login <password>: Administrator login\n"));
		return;
	}

	if (reqpass)
		Z_Free(reqpass);

	pw = COM_Argv(1);
	reqpass = Z_StrDup(pw);

	CONS_Printf(M_GetText("Sending login... (Notice only given if password is correct.)\n"));

	netbuffer->packettype = PT_LOGIN;
	HSendPacket(servernode, true, 0, 0);
}

boolean IsPlayerAdmin(INT32 playernum)
{
#if defined (DEVELOP)
	return playernum != serverplayer;
#else
	INT32 i;
	for (i = 0; i < MAXPLAYERS; i++)
		if (playernum == adminplayers[i])
			return true;

	return false;
#endif
}

void SetAdminPlayer(INT32 playernum)
{
	INT32 i;
	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (playernum == adminplayers[i])
			return; // Player is already admin

		if (adminplayers[i] == -1)
		{
			adminplayers[i] = playernum; // Set the player to a free spot
			break; // End the loop now. If it keeps going, the same player might get assigned to two slots.
		}
	}
}

void ClearAdminPlayers(void)
{
	memset(adminplayers, -1, sizeof(adminplayers));
}

void RemoveAdminPlayer(INT32 playernum)
{
	INT32 i;
	for (i = 0; i < MAXPLAYERS; i++)
		if (playernum == adminplayers[i])
			adminplayers[i] = -1;
}

static void Command_Verify_f(void)
{
	char buf[8]; // Should be plenty
	char *temp;
	INT32 playernum;

	if (client)
	{
		CONS_Printf(M_GetText("Only the server can use this.\n"));
		return;
	}

	if (!netgame)
	{
		CONS_Printf(M_GetText("This only works in a netgame.\n"));
		return;
	}

	if (COM_Argc() != 2)
	{
		CONS_Printf(M_GetText("promote <playernum>: give admin privileges to a player\n"));
		return;
	}

	strlcpy(buf, COM_Argv(1), sizeof (buf));

	playernum = atoi(buf);

	temp = buf;

	WRITEUINT8(temp, playernum);

	if (playeringame[playernum])
		SendNetXCmd(XD_VERIFIED, buf, 1);
}

static void Got_Verification(UINT8 **cp, INT32 playernum)
{
	INT16 num = READUINT8(*cp);

	if (playernum != serverplayer) // it's not from the server (hacker or bug)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal verification received from %s (serverplayer is %s)\n"), player_names[playernum], player_names[serverplayer]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	SetAdminPlayer(num);

	if (num != consoleplayer)
		return;

	CONS_Printf(M_GetText("You are now a server administrator.\n"));
}

static void Command_RemoveAdmin_f(void)
{
	char buf[8]; // Should be plenty
	char *temp;
	INT32 playernum;

	if (client)
	{
		CONS_Printf(M_GetText("Only the server can use this.\n"));
		return;
	}

	if (COM_Argc() != 2)
	{
		CONS_Printf(M_GetText("demote <playernum>: remove admin privileges from a player\n"));
		return;
	}

	strlcpy(buf, COM_Argv(1), sizeof(buf));

	playernum = atoi(buf);

	temp = buf;

	WRITEUINT8(temp, playernum);

	if (playeringame[playernum])
		SendNetXCmd(XD_DEMOTED, buf, 1);
}

static void Got_Removal(UINT8 **cp, INT32 playernum)
{
	UINT8 num = READUINT8(*cp);

	if (playernum != serverplayer) // it's not from the server (hacker or bug)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal demotion received from %s (serverplayer is %s)\n"), player_names[playernum], player_names[serverplayer]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	RemoveAdminPlayer(num);

	if (num != consoleplayer)
		return;

	CONS_Printf(M_GetText("You are no longer a server administrator.\n"));
}

void Schedule_Run(void)
{
	size_t i;

	if (schedule_len == 0)
	{
		// No scheduled tasks to run.
		return;
	}

	if (!cv_schedule.value)
	{
		// We don't WANT to run tasks.
		return;
	}

	if (K_CanChangeRules(false) == false)
	{
		// Don't engage in automation while in a restricted context.
		return;
	}

	for (i = 0; i < schedule_len; i++)
	{
		scheduleTask_t *task = schedule[i];

		if (task == NULL)
		{
			// Shouldn't happen.
			break;
		}

		if (task->timer > 0)
		{
			task->timer--;
		}

		if (task->timer == 0)
		{
			// Reset timer
			task->timer = task->basetime;

			// Run command for server
			if (server)
			{
				CONS_Printf(
					"%d seconds elapsed, running \"" "\x82" "%s" "\x80" "\".\n",
					task->basetime,
					task->command
				);

				COM_BufAddText(task->command);
				COM_BufAddText("\n");
			}
		}
	}
}

void Schedule_Insert(scheduleTask_t *addTask)
{
	if (schedule_len >= schedule_size)
	{
		if (schedule_size == 0)
		{
			schedule_size = 8;
		}
		else
		{
			schedule_size *= 2;
		}

		schedule = Z_ReallocAlign(
			(void*) schedule,
			sizeof(scheduleTask_t*) * schedule_size,
			PU_STATIC,
			NULL,
			sizeof(scheduleTask_t*) * 8
		);
	}

	schedule[schedule_len] = addTask;
	schedule_len++;
}

void Schedule_Add(INT16 basetime, INT16 timeleft, const char *command)
{
	scheduleTask_t *task = (scheduleTask_t*) Z_CallocAlign(
		sizeof(scheduleTask_t),
		PU_STATIC,
		NULL,
		sizeof(scheduleTask_t) * 8
	);

	task->basetime = basetime;
	task->timer = timeleft;
	task->command = Z_StrDup(command);

	Schedule_Insert(task);
}

void Schedule_Clear(void)
{
	size_t i;

	for (i = 0; i < schedule_len; i++)
	{
		scheduleTask_t *task = schedule[i];

		if (task->command)
			Z_Free(task->command);
	}

	schedule_len = 0;
	schedule_size = 0;
	schedule = NULL;
}

void Automate_Set(automateEvents_t type, const char *command)
{
	if (!server)
	{
		// Since there's no list command or anything for this,
		// we don't need this code to run for anyone but the server.
		return;
	}

	if (automate_commands[type] != NULL)
	{
		// Free the old command.
		Z_Free(automate_commands[type]);
	}

	if (command == NULL || strlen(command) == 0)
	{
		// Remove the command.
		automate_commands[type] = NULL;
	}
	else
	{
		// New command.
		automate_commands[type] = Z_StrDup(command);
	}
}

void Automate_Run(automateEvents_t type)
{
	if (!server)
	{
		// Only the server should be doing this.
		return;
	}

	if (K_CanChangeRules(false) == false)
	{
		// Don't engage in automation while in a restricted context.
		return;
	}

#ifdef PARANOIA
	if (type >= AEV__MAX)
	{
		// Shouldn't happen.
		I_Error("Attempted to run invalid automation type.");
		return;
	}
#endif

	if (!cv_automate.value)
	{
		// We don't want to run automation.
		return;
	}

	if (automate_commands[type] == NULL)
	{
		// No command to run.
		return;
	}

	CONS_Printf(
		"Running %s automate command \"" "\x82" "%s" "\x80" "\"...\n",
		automate_names[type],
		automate_commands[type]
	);

	COM_BufAddText(automate_commands[type]);
	COM_BufAddText("\n");
}

void Automate_Clear(void)
{
	size_t i;

	for (i = 0; i < AEV__MAX; i++)
	{
		Automate_Set(i, NULL);
	}
}

static void Command_MotD_f(void)
{
	size_t i, j;
	char *mymotd;

	if ((j = COM_Argc()) < 2)
	{
		CONS_Printf(M_GetText("motd <message>: Set a message that clients see upon join.\n"));
		return;
	}

	if (!(server || (IsPlayerAdmin(consoleplayer))))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	mymotd = Z_Malloc(sizeof(motd), PU_STATIC, NULL);

	if (!mymotd)
		return;
		//I_Error("Command_MotD_f: Out of memory!\n"); // idk if this aint a bit too much lel

	strlcpy(mymotd, COM_Argv(1), sizeof(motd));

	for (i = 2; i < j; i++)
	{
		strlcat(mymotd, " ", sizeof(motd));
		strlcat(mymotd, COM_Argv(i), sizeof(motd));
	}

	// Disallow non-printing characters and semicolons.
	for (i = 0; mymotd[i] != '\0'; i++)
	{
		if (!isprint(mymotd[i]) || mymotd[i] == ';')
		{
			Z_Free(mymotd);
			return;
		}
	}

	if ((netgame || multiplayer) && client)
	{
		SendNetXCmd(XD_SETMOTD, mymotd, i); // send the actual size of the motd string, not the full buffer's size
	}
	else
	{
		strcpy(motd, mymotd);
		CONS_Printf(M_GetText("Message of the day set.\n"));
	}

	Z_Free(mymotd);
}

static void Got_MotD_f(UINT8 **cp, INT32 playernum)
{
	char *mymotd = Z_Malloc(sizeof(motd), PU_STATIC, NULL);
	INT32 i;
	boolean kick = false;

	READSTRINGN(*cp, mymotd, sizeof(motd));

	// Disallow non-printing characters and semicolons.
	for (i = 0; mymotd[i] != '\0'; i++)
		if (!isprint(mymotd[i]) || mymotd[i] == ';')
			kick = true;

	if ((playernum != serverplayer && !IsPlayerAdmin(playernum)) || kick)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal motd change received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		Z_Free(mymotd);
		return;
	}

	strcpy(motd, mymotd);

	CONS_Printf(M_GetText("Message of the day set.\n"));

	Z_Free(mymotd);
}

static void Command_RunSOC(void)
{
	const char *fn;
	char buf[255];
	size_t length = 0;

	if (COM_Argc() != 2)
	{
		CONS_Printf(M_GetText("runsoc <socfile.soc> or <lumpname>: run a soc\n"));
		return;
	}
	else
		fn = COM_Argv(1);

	if (netgame && !(server || IsPlayerAdmin(consoleplayer)))
	{
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		return;
	}

	if (!(netgame || multiplayer))
	{
		if (!P_RunSOC(fn))
			CONS_Printf(M_GetText("Could not find SOC.\n"));
		else
			G_SetGameModified(multiplayer, false);
		return;
	}

	nameonly(strcpy(buf, fn));
	length = strlen(buf)+1;

	SendNetXCmd(XD_RUNSOC, buf, length);
}

static void Got_RunSOCcmd(UINT8 **cp, INT32 playernum)
{
	char filename[256];
	filestatus_t ncs = FS_NOTCHECKED;

	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal runsoc command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	READSTRINGN(*cp, filename, 255);

	// Maybe add hashing support?
	if (strstr(filename, ".soc") != NULL)
	{
		ncs = findfile(filename, 0, true);

		if (ncs != FS_FOUND)
		{
			Command_ExitGame_f();
			if (ncs == FS_NOTFOUND)
			{
				CONS_Printf(M_GetText("The server tried to add %s,\nbut you don't have this file.\nYou need to find it in order\nto play on this server.\n"), filename);
				M_StartMessage(va("The server added a file\n(%s)\nthat you do not have.\n\nPress ESC\n",filename), NULL, MM_NOTHING);
			}
			else
			{
				CONS_Printf(M_GetText("Unknown error finding soc file (%s) the server added.\n"), filename);
				M_StartMessage(va("Unknown error trying to load a file\nthat the server added\n(%s).\n\nPress ESC\n",filename), NULL, MM_NOTHING);
			}
			return;
		}
	}

	P_RunSOC(filename);
	G_SetGameModified(true, false);
}

static void Command_Addfilelocal(void)
{
	const char *fn;
	INT32 i;
	size_t docompat = COM_CheckPartialParm("-c");
	size_t nocompat = COM_CheckPartialParm("-n");
	wadcompat_t compat = WC_AUTO;

	if (docompat && nocompat)
	{
		CONS_Printf(M_GetText("Please specify only one of -c or -n\n"));
		return;
	}
	else if (docompat)
		compat = WC_ON;
	else if (nocompat)
		compat = WC_OFF;

	if (COM_Argc() != 2u + (docompat || nocompat))
	{
		CONS_Printf(M_GetText("addfilelocal [-c|-n] <filename.pk3/wad/lua/soc>: Load local add-on\n"
									"-c forces compatmode, -n disables it.\n"));
		return;
	}
	else
		fn = COM_Argv(1);

	// Disallow non-printing characters and semicolons.
	for (i = 0; fn[i] != '\0'; i++)
		if (!isprint(fn[i]) || fn[i] == ';')
			return;

	// Add any wad file, ignoring checks for if it contains complex things like
	// lua. Great for complex but client-side customizations, like different
	// level cards or anything like that.
	P_AddWadFile(fn, compat, true);
}

/** Adds a pwad at runtime.
  * Searches for sounds, maps, music, new images.
  */
static void Command_Addfile(void)
{
	size_t argc = COM_Argc(); // amount of arguments total
	size_t curarg; // current argument index

	const char *addedfiles[argc]; // list of filenames already processed
	size_t numfilesadded = 0; // the amount of filenames processed

	size_t docompat = COM_CheckPartialParm("-c");
	size_t nocompat = COM_CheckPartialParm("-n");
	wadcompat_t compat = WC_AUTO;

	if (docompat && nocompat)
	{
		CONS_Printf(M_GetText("Please specify only one of -c or -n\n"));
		return;
	}
	else if (docompat)
		compat = WC_ON;
	else if (nocompat)
		compat = WC_OFF;

	if (argc < 2u + (docompat || nocompat))
	{
		CONS_Printf(M_GetText("addfile [-c|-n] <filename.pk3/wad/lua/soc> [filename2...]: Load add-ons\n"
		                      "-c forces compatmode, -n disables it.\n"));
		return;
	}

	// start at one to skip command name
	for (curarg = 1; curarg < argc; curarg++)
	{
		const char *fn, *p;
		char buf[256];
		char *buf_p = buf;
		INT32 i;
		size_t ii;
		int musiconly; // W_VerifyNMUSlumps isn't boolean
		boolean fileadded = false;

		if (curarg == docompat || curarg == nocompat)
			continue;

		fn = COM_Argv(curarg);

		// For the amount of filenames previously processed...
		for (ii = 0; ii < numfilesadded; ii++)
		{
			// If this is one of them, don't try to add it.
			if (fastcmp(fn, addedfiles[ii]))
			{
				fileadded = true;
				break;
			}
		}

		// If we've added this one, skip to the next one.
		if (fileadded)
		{
			CONS_Alert(CONS_WARNING, M_GetText("Already processed %s, skipping\n"), fn);
			continue;
		}

		// Disallow non-printing characters and semicolons.
		for (i = 0; fn[i] != '\0'; i++)
			if (!isprint(fn[i]) || fn[i] == ';')
				return;

		musiconly = W_VerifyNMUSlumps(fn, false);

		if (musiconly == -1)
		{
			addedfiles[numfilesadded++] = fn;
			continue;
		}

		if (!musiconly)
		{
			// ... But only so long as they contain nothing more then music and sprites.
			if (netgame && !(server || IsPlayerAdmin(consoleplayer)))
			{
				CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
				continue;
			}
			G_SetGameModified(multiplayer, false);
		}

		// Add file on your client directly if it is trivial, or you aren't in a netgame.
		if (!netgame|| musiconly)
		{
			P_AddWadFile(fn, compat, false);
			addedfiles[numfilesadded++] = fn;
			continue;
		}

		p = fn+strlen(fn);
		while(--p >= fn)
			if (*p == '\\' || *p == '/' || *p == ':')
				break;
		++p;

		// check total packet size and no of files currently loaded
		// See W_LoadWadFile in w_wad.c
		if (numwadfiles >= MAX_WADFILES)
		{
			CONS_Alert(CONS_ERROR, M_GetText("Too many files loaded to add %s\n"), fn);
			return;
		}

		WRITESTRINGN(buf_p,p,240);

		// calculate and check the hash
		{
			UINT64 filehash;
#ifdef NOFILEHASH
			filehash = 0;
#else
			boolean valid = true;
			if (!W_MakeFileHash(fn, true, &filehash))
				continue; // file not found

			for (i = 0; i < numwadfiles; i++)
			{
				if (wadfiles[i]->hash == filehash)
				{
					CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), fn);
					valid = false;
					break;
				}
			}

			if (valid == false)
			{
				continue;
			}
#endif
			WRITEUINT64(buf_p, filehash);
		}

		WRITEUINT8(buf_p, compat);

		addedfiles[numfilesadded++] = fn;

		if (IsPlayerAdmin(consoleplayer) && (!server)) // Request to add file
			SendNetXCmd(XD_REQADDFILE, buf, buf_p - buf);
		else
			SendNetXCmd(XD_ADDFILE, buf, buf_p - buf);
	}
}

static void Got_RequestAddfilecmd(UINT8 **cp, INT32 playernum)
{
	char filename[241];
	filestatus_t ncs = FS_NOTCHECKED;
	UINT64 filehash;
	boolean kick = false;
	boolean toomany = false;
	INT32 i,j;

	READSTRINGN(*cp, filename, 240);
	filehash = READUINT64(cp);
	wadcompat_t compat = READUINT8(*cp);

	// Only the server processes this message.
	if (client)
		return;

	// Disallow non-printing characters and semicolons.
	for (i = 0; filename[i] != '\0'; i++)
		if (!isprint(filename[i]) || filename[i] == ';')
			kick = true;

	if ((playernum != serverplayer && !IsPlayerAdmin(playernum)) || kick)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal addfile command received from %s\n"), player_names[playernum]);
		SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	// See W_LoadWadFile in w_wad.c
	if (numwadfiles >= MAX_WADFILES)
		toomany = true;
	else
		ncs = findfile(filename, filehash, true);

	if (ncs != FS_FOUND || toomany)
	{
		char message[275];

		if (toomany)
			sprintf(message, M_GetText("Too many files loaded to add %s\n"), filename);
		else if (ncs == FS_NOTFOUND)
			sprintf(message, M_GetText("The server doesn't have %s\n"), filename);
		else if (ncs == FS_BADHASH)
			sprintf(message, M_GetText("Checksum mismatch on %s\n"), filename);
		else
			sprintf(message, M_GetText("Unknown error finding wad file (%s)\n"), filename);

		CONS_Printf("%s",message);

		for (j = 0; j < MAXPLAYERS; j++)
			if (adminplayers[j])
				COM_BufAddText(va("sayto %d %s", adminplayers[j], message));

		return;
	}

	COM_BufAddText(va("addfile %s %s\n", compat == WC_ON ? "-c" : (compat == WC_OFF ? "-n" : ""), filename));
}

static void Got_Addfilecmd(UINT8 **cp, INT32 playernum)
{
	char filename[241];
	filestatus_t ncs = FS_NOTCHECKED;
	UINT64 filehash;

	READSTRINGN(*cp, filename, 240);
	filehash = READUINT64(*cp);
	wadcompat_t compat = READUINT8(*cp);

	if (playernum != serverplayer)
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal addfile command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	ncs = findfile(filename, filehash, true);

	if (ncs != FS_FOUND || !P_AddWadFile(filename, compat, false))
	{
		Command_ExitGame_f();
		if (ncs == FS_FOUND)
		{
			CONS_Printf(M_GetText("The server tried to add %s,\nbut you have too many files added.\nRestart the game to clear loaded files\nand play on this server."), filename);
			M_StartMessage(va("The server added a file \n(%s)\nbut you have too many files added.\nRestart the game to clear loaded files.\n\nPress ESC\n",filename), NULL, MM_NOTHING);
		}
		else if (ncs == FS_NOTFOUND)
		{
			CONS_Printf(M_GetText("The server tried to add %s,\nbut you don't have this file.\nYou need to find it in order\nto play on this server."), filename);
			M_StartMessage(va("The server added a file \n(%s)\nthat you do not have.\n\nPress ESC\n",filename), NULL, MM_NOTHING);
		}
		else if (ncs == FS_BADHASH)
		{
			CONS_Printf(M_GetText("Checksum mismatch while loading %s.\nMake sure you have the copy of\nthis file that the server has.\n"), filename);
			M_StartMessage(va("Checksum mismatch while loading \n%s.\nThe server seems to have a\ndifferent version of this file.\n\nPress ESC\n",filename), NULL, MM_NOTHING);
		}
		else
		{
			CONS_Printf(M_GetText("Unknown error finding wad file (%s) the server added.\n"), filename);
			M_StartMessage(va("Unknown error trying to load a file\nthat the server added \n(%s).\n\nPress ESC\n",filename), NULL, MM_NOTHING);
		}
		return;
	}

	G_SetGameModified(true, false);
}

static void Command_ListWADS_f(void)
{
	INT32 i = numwadfiles;
	char *tempname;
	CONS_Printf(M_GetText("There are %d wads loaded:\n"),numwadfiles);
	for (i--; i >= 0; i--)
	{
		nameonly(tempname = va("%s", wadfiles[i]->filename));
		if (!i)
			CONS_Printf("\x82 IWAD\x80: %s\n", tempname);
		else if (i < NUMMAINWADS)
			CONS_Printf("\x82 * %.2d\x80: %s\n", i, tempname);
		else if (!wadfiles[i]->important)
			CONS_Printf("\x86   %.2d: %s\n", i, tempname);
		else if (wadfiles[i]->compatmode)
			CONS_Printf("\x8a   %.2d: %s\n", i, tempname);
		else
			CONS_Printf("   %.2d: %s\n", i, tempname);
	}
}

static void Command_LocateLump_f(void)
{
	if (COM_Argc() == 1)
	{
		CONS_Printf("Usage: locatelump <lump1>[ <lump2>[ ...]]\n");
		return;
	}

	for (size_t i = 1; i < COM_Argc(); ++i)
	{
		const char *name = COM_Argv(i);

		lumpnum_t num = W_CheckNumForName(name);

		const char *wadname = "(not found)";

		if (num != LUMPERROR)
		{
			wadname = wadfiles[WADFILENUM(num)]->filename;
		}

		CONS_Printf("%s - %s\n", name, wadname);
	}
}

#define MAXDOOMEDNUM 65536

static void Command_ListDoomednums_f(void)
{
	INT32 i, j, focusstart = 0, focusend = 0;
	INT32 argc = COM_Argc(), argstart = 0;
	INT32 table[MAXDOOMEDNUM];
	boolean nodoubles = false;
	UINT8 doubles[(MAXDOOMEDNUM+8/8)];

	if (argc > 1)
	{
		nodoubles = (fastcmp(COM_Argv(1), "-nodoubles"));
		if (nodoubles)
		{
			argc--;
			argstart++;
		}
	}

	switch (argc)
	{
		case 1:
			focusend = MAXDOOMEDNUM;
			break;
		case 3:
			focusend = atoi(COM_Argv(argstart+2));
			if (focusend < 1 || focusend > MAXDOOMEDNUM)
			{
				CONS_Printf("arg 2: doomednum \x82""%d \x85out of range (1-4095)\n", focusend);
				return;
			}
			//FALLTHRU
		case 2:
			focusstart = atoi(COM_Argv(argstart+1));
			if (focusstart < 1 || focusstart > MAXDOOMEDNUM)
			{
				CONS_Printf("arg 1: doomednum \x82""%d \x85out of range (1-4095)\n", focusstart);
				return;
			}
			if (!focusend)
				focusend = focusstart;
			else if (focusend < focusstart) // silently and helpfully swap.
			{
				j = focusstart;
				focusstart = focusend;
				focusend = j;
			}
			break;
		default:
			CONS_Printf("listmapthings: \x86too many arguments!\n");
			return;
	}

	// see P_SpawnNonMobjMapThing
	memset(table, 0, sizeof(table));
	memset(doubles, 0, sizeof(doubles));
	for (i = 1; i <= MAXPLAYERS; i++)
		table[i-1] = MT_PLAYER; // playerstarts
	table[33-1] = table[34-1] = table[35-1] = MT_PLAYER; // battle/team starts
	table[750-1] = table[777-1] = table[778-1] = MT_UNKNOWN; // slopes
	for (i = 600; i <= 609; i++)
		table[i-1] = MT_RING; // placement patterns
	table[1705-1] = table[1713-1] = MT_HOOP; // types of hoop

	CONS_Printf("\x82""Checking for double defines...\n");
	for (i = 1; i < MT_FIRSTFREESLOT+NUMMOBJFREESLOTS; i++)
	{
		j = mobjinfo[i].doomednum;
		if (j < (focusstart ? focusstart : 1) || j > focusend)
			continue;
		if (table[--j])
		{
			doubles[j/8] |= 1<<(j&7);
			CONS_Printf("	doomednum \x82""%d""\x80 is \x85""double-defined\x80 by ", j+1);
			if (i < MT_FIRSTFREESLOT)
			{
				CONS_Printf("\x87""hardcode MT_%s <-- MAJOR ERROR\n", DEH_MobjtypeName(i));
				continue;
			}
			CONS_Printf("\x81""freeslot MT_""%s\n", DEH_MobjtypeName(i));
			continue;
		}
		table[j] = i;
	}
	CONS_Printf("\x82Printing doomednum usage...\n");
	if (!focusstart)
	{
		i = 35; // skip MT_PLAYER spam
		if (!nodoubles)
			CONS_Printf("	doomednums \x82""1-35""\x80 are used by ""\x87""hardcode MT_PLAYER\n");
	}
	else
		i = focusstart-1;

	for (; i < focusend; i++)
	{
		if (nodoubles && !(doubles[i/8] & 1<<(i&7)))
			continue;
		if (!table[i])
		{
			if (focusstart)
			{
				CONS_Printf("	doomednum \x82""%d""\x80 is \x83""free!", i+1);
				if (i < 99) // above the humble crawla? how dare you
					CONS_Printf(" (Don't freeslot this low...)");
				CONS_Printf("\n");
			}
			continue;
		}
		CONS_Printf("	doomednum \x82""%d""\x80 is used by ", i+1);

		for (j = 0; j < MT_FIRSTFREESLOT+NUMMOBJFREESLOTS; j++)
		{
			if (mobjinfo[j].doomednum != i+1)
				continue;

			if (j < MT_FIRSTFREESLOT)
			{
				CONS_Printf("\x87""hardcode MT_%s\n", DEH_MobjtypeName(j));
				continue;
			}
			else
			{
				CONS_Printf("\x81""freeslot MT_""%s\n", DEH_MobjtypeName(j));
				continue;
			}

			CONS_Printf("\x87""?????\n");
		}
	}
}

#undef MAXDOOMEDNUM

// =========================================================================
//                            MISC. COMMANDS
// =========================================================================

/** Prints program version.
  */
static void Command_Version_f(void)
{
#if (defined DEVELOP) || (defined COMMITVERSION)
	CONS_Printf("BlanKart %s-%s (%s %s)\n", D_GetFancyBranchName(), comprevision, compdate, comptime);
#else
	CONS_Printf("BlanKart %s (%s %s %s %s) ", VERSIONSTRING, compdate, comptime, comprevision, D_GetFancyBranchName());
#endif

	// Base library
#if defined( HAVE_SDL)
	CONS_Printf("SDL ");
#endif

	// OS
	// Would be nice to use SDL_GetPlatform for this
#if defined (_WIN32) || defined (_WIN64)
	CONS_Printf("Windows ");
#elif defined(__linux__)
	CONS_Printf("Linux ");
#elif defined(MACOSX)
	CONS_Printf("macOS ");
#elif defined(UNIXCOMMON)
	CONS_Printf("Unix (Common) ");
#else
	CONS_Printf("Other OS ");
#endif

	// Bitness
	if (sizeof(void*) == 4)
		CONS_Printf("32-bit ");
	else if (sizeof(void*) == 8)
		CONS_Printf("64-bit ");
	else // 16-bit? 128-bit?
		CONS_Printf("Bits Unknown ");

	// Debug build
#ifdef _DEBUG
	CONS_Printf("\x85" "DEBUG " "\x80");
#endif

	// DEVELOP build
#if defined(DEVELOP)
	CONS_Printf("\x87" "DEVELOP " "\x80");
#endif

	if (compuncommitted)
		CONS_Printf("\x85" "! UNCOMMITTED CHANGES ! " "\x80");

	CONS_Printf("\n");
}

#ifdef UPDATE_ALERT
static void Command_ModDetails_f(void)
{
	CONS_Printf(M_GetText("Mod ID: %d\nMod Version: %d\nCode Base:%d\n"), MODID, MODVERSION, CODEBASE);
}
#endif

// Returns current gametype being used.
//
static void Command_ShowGametype_f(void)
{
	const char *gametypestr = NULL;

	// get name string for current gametype
	if (gametype >= 0 && gametype < numgametypes)
		gametypestr = gametypes[gametype]->name;

	if (gametypestr)
		CONS_Printf(M_GetText("Current gametype is %s\n"), gametypestr);
	else // string for current gametype was not found above (should never happen)
		CONS_Printf(M_GetText("Unknown gametype set (%d)\n"), gametype);
}

/** Plays the intro.
  */
static void Command_Playintro_f(void)
{
	if (netgame)
		return;

	if (dirmenu)
		closefilemenu(true);

	F_StartIntro();
}

/** Quits the game immediately.
  */
FUNCNORETURN static ATTRNORETURN void Command_Quit_f(void)
{
	LUA_HookBool(true, HOOK(GameQuit));
	I_Quit();
}

void ItemFinder_OnChange(void)
{
	if (!cv_itemfinder.value)
		return; // it's fine.

	if (!M_SecretUnlocked(SECRET_ITEMFINDER))
	{
		CONS_Printf(M_GetText("You haven't earned this yet.\n"));
		CV_StealthSetValue(&cv_itemfinder, 0);
		return;
	}
	else if (netgame || multiplayer)
	{
		CONS_Printf(M_GetText("This only works in single player.\n"));
		CV_StealthSetValue(&cv_itemfinder, 0);
		return;
	}
}

/** Deals with a pointlimit change by printing the change to the console.
  * If the gametype is single player, cooperative, or race, the pointlimit is
  * silently disabled again.
  *
  * Timelimit and pointlimit can be used at the same time.
  *
  * We don't check immediately for the pointlimit having been reached,
  * because you would get "caught" when turning it up in the menu.
  * \sa cv_pointlimit, TimeLimit_OnChange
  * \author Graue <graue@oceanbase.org>
  */
static void PointLimit_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	// Don't allow pointlimit in non-pointlimited gametypes!
	if (server && Playing() && !(gametypes[gametype]->rules & GTR_POINTLIMIT))
	{
		if (cv_pointlimit.value)
			CV_StealthSetValue(&cv_pointlimit, 0);
		return;
	}

	if (cv_pointlimit.value)
	{
		CONS_Printf(M_GetText("Levels will end after %s scores %d point%s.\n"),
			G_GametypeHasTeams() ? M_GetText("a team") : M_GetText("someone"),
			cv_pointlimit.value,
			cv_pointlimit.value > 1 ? "s" : "");
	}
	else
		CONS_Printf(M_GetText("Point limit disabled\n"));
}

static void NetTimeout_OnChange(void)
{
	connectiontimeout = (tic_t)cv_nettimeout.value;
}

static void JoinTimeout_OnChange(void)
{
	jointimeout = (tic_t)cv_jointimeout.value;
}

static void
Lagless_OnChange (void)
{
	if (gamestate == GS_LEVEL)
		server_lagless = cv_lagless.value;
}

UINT32 timelimitintics = 0;
UINT32 extratimeintics = 0;
UINT32 secretextratime = 0;

/** Deals with a timelimit change by printing the change to the console.
  * If the gametype is single player, cooperative, or race, the timelimit is
  * silently disabled again.
  *
  * Timelimit and pointlimit can be used at the same time.
  *
  * \sa cv_timelimit, PointLimit_OnChange
  */
static void TimeLimit_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (gamestate == GS_LEVEL && leveltime < starttime)
	{
		if (cv_timelimit.value)
		{
			CONS_Printf(M_GetText("Time limit has been set to %d minute%s.\n"), cv_timelimit.value,cv_timelimit.value == 1 ? "" : "s");
		}
		else
		{
			CONS_Printf(M_GetText("Time limit has been disabled.\n"));
		}

		timelimitintics = cv_timelimit.value * (60*TICRATE);
		extratimeintics = secretextratime = 0;

#ifdef HAVE_DISCORDRPC
		DRPC_UpdatePresence();
#endif
	}
	else
	{
		if (cv_timelimit.value)
		{
			CONS_Printf(M_GetText("Time limit will be %d minute%s next round.\n"), cv_timelimit.value,cv_timelimit.value == 1 ? "" : "s");
		}
		else
		{
			CONS_Printf(M_GetText("Time limit will be disabled next round.\n"));
		}
	}
}

/** Adjusts certain settings to match a changed gametype.
  *
  * \param lastgametype The gametype we were playing before now.
  * \sa D_MapChange
  * \author Graue <graue@oceanbase.org>
  * \todo Get rid of the hardcoded stuff, ugly stuff, etc.
  */
void D_GameTypeChanged(INT32 lastgametype)
{
	if (netgame)
	{
		const char *oldgt = NULL, *newgt = NULL;

		if (lastgametype >= 0 && lastgametype < numgametypes)
			oldgt = gametypes[lastgametype]->name;
		if (gametype >= 0 && gametype < numgametypes)
			newgt = gametypes[gametype]->name;

		if (oldgt && newgt && (lastgametype != gametype))
			CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), oldgt, newgt);
	}
	// Only do the following as the server, not as remote admin.
	// There will always be a server, and this only needs to be done once.
	if (server && multiplayer)
	{
		if (!cv_timelimit.changed) // user hasn't changed limits
		{
			CV_SetValue(&cv_timelimit, gametypes[gametype]->timelimit);
		}
		if (!cv_pointlimit.changed)
		{
			CV_SetValue(&cv_pointlimit, gametypes[gametype]->pointlimit);
		}
	}

	// don't retain teams in other modes or between changes from ctf to team match.
	// also, stop any and all forms of team scrambling that might otherwise take place.
	if (G_GametypeHasTeams())
	{
		INT32 i;
		for (i = 0; i < MAXPLAYERS; i++)
			if (playeringame[i])
				players[i].ctfteam = 0;

		if (server || (IsPlayerAdmin(consoleplayer)))
		{
			CV_StealthSetValue(&cv_teamscramble, 0);
			teamscramble = 0;
		}
	}
}

static void Gravity_OnChange(void)
{
	if (!M_SecretUnlocked(SECRET_PANDORA) && !netgame && !cht_debug
		&& !fastcmp(cv_gravity.string, cv_gravity.defaultvalue))
	{
		CONS_Printf(M_GetText("You haven't earned this yet.\n"));
		CV_StealthSet(&cv_gravity, cv_gravity.defaultvalue);
		return;
	}
#ifndef NETGAME_GRAVITY
	if(netgame)
	{
		CV_StealthSet(&cv_gravity, cv_gravity.defaultvalue);
		return;
	}
#endif

	if (!CV_IsSetToDefault(&cv_gravity))
		G_SetGameModified(multiplayer, true);
	gravity = cv_gravity.value;
}

static void SoundTest_OnChange(void)
{
	INT32 sfxfreeint = (INT32)sfxfree;
	if (cv_soundtest.value < 0)
	{
		CV_SetValue(&cv_soundtest, sfxfreeint-1);
		return;
	}

	if (cv_soundtest.value >= sfxfreeint)
	{
		CV_SetValue(&cv_soundtest, 0);
		return;
	}

	S_StopSounds();
	S_StartSound(NULL, cv_soundtest.value);
}

static void AutoBalance_OnChange(void)
{
	autobalance = (INT16)cv_autobalance.value;
}

static void TeamScramble_OnChange(void)
{
	INT16 i = 0, j = 0, playercount = 0;
	boolean repick = true;
	INT32 blue = 0, red = 0;
	INT32 maxcomposition = 0;
	INT16 newteam = 0;
	INT32 retries = 0;
	boolean success = false;

	// Don't trigger outside level or intermission!
	if (!(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION || gamestate == GS_VOTING))
		return;

	if (!cv_teamscramble.value)
		teamscramble = 0;

	if (!G_GametypeHasTeams() && (server || IsPlayerAdmin(consoleplayer)))
	{
		CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used in this gametype.\n"));
		CV_StealthSetValue(&cv_teamscramble, 0);
		return;
	}

	// If a team scramble is already in progress, do not allow another one to be started!
	if (teamscramble)
		return;

retryscramble:

	// Clear related global variables. These will get used again in p_tick.c/y_inter.c as the teams are scrambled.
	memset(&scrambleplayers, 0, sizeof(scrambleplayers));
	memset(&scrambleteams, 0, sizeof(scrambleplayers));
	scrambletotal = scramblecount = 0;
	blue = red = newteam = playercount = 0;
	repick = true;

	// Put each player's node in the array.
	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (playeringame[i] && !players[i].spectator)
		{
			scrambleplayers[playercount] = i;
			playercount++;
		}
	}

	if (playercount < 2)
	{
		CV_StealthSetValue(&cv_teamscramble, 0);
		return; // Don't scramble one or zero players.
	}

	// Randomly place players on teams.
	if (cv_teamscramble.value == 1)
	{
		maxcomposition = playercount / 2;

		// Now randomly assign players to teams.
		// If the teams get out of hand, assign the rest to the other team.
		for (i = 0; i < playercount; i++)
		{
			if (repick)
				newteam = (INT16)((M_RandomByte() % 2) + 1);

			// One team has the most players they can get, assign the rest to the other team.
			if (red == maxcomposition || blue == maxcomposition)
			{
				if (red == maxcomposition)
					newteam = 2;
				else //if (blue == maxcomposition)
					newteam = 1;

				repick = false;
			}

			scrambleteams[i] = newteam;

			if (newteam == 1)
				red++;
			else
				blue++;
		}
	}
	else if (cv_teamscramble.value == 2) // Same as before, except split teams based on current score.
	{
		// Now, sort the array based on points scored.
		for (i = 1; i < playercount; i++)
		{
			for (j = i; j < playercount; j++)
			{
				INT16 tempplayer = 0;

				if ((players[scrambleplayers[i-1]].score > players[scrambleplayers[j]].score))
				{
					tempplayer = scrambleplayers[i-1];
					scrambleplayers[i-1] = scrambleplayers[j];
					scrambleplayers[j] = tempplayer;
				}
			}
		}

		// Now assign players to teams based on score. Scramble in pairs.
		// If there is an odd number, one team will end up with the unlucky slob who has no points. =(
		for (i = 0; i < playercount; i++)
		{
			if (repick)
			{
				newteam = (INT16)((M_RandomByte() % 2) + 1);
				repick = false;
			}
			// (i != 2) means it does ABBABABA, instead of ABABABAB.
			// Team A gets 1st, 4th, 6th, 8th.
			// Team B gets 2nd, 3rd, 5th, 7th.
			// So 1st on one team, 2nd/3rd on the other, then alternates afterwards.
			// Sounds strange on paper, but works really well in practice!
			else if (i != 2)
			{
				// We will only randomly pick the team for the first guy.
				// Otherwise, just alternate back and forth, distributing players.
				newteam = 3 - newteam;
			}

			scrambleteams[i] = newteam;
		}
	}

	// Check to see if our random selection actually
	// changed anybody. If not, we run through and try again.
	for (i = 0; i < playercount; i++)
	{
		if (players[scrambleplayers[i]].ctfteam != scrambleteams[i])
			success = true;
	}

	if (!success && retries < 5)
	{
		retries++;
		goto retryscramble; //try again
	}

	// Display a witty message, but only during scrambles specifically triggered by an admin.
	if (cv_teamscramble.value)
	{
		scrambletotal = playercount;
		teamscramble = (INT16)cv_teamscramble.value;

		if (!(gamestate == GS_INTERMISSION && cv_scrambleonchange.value))
			CONS_Printf(M_GetText("Teams will be scrambled next round.\n"));
	}
}

static void Command_Showmap_f(void)
{
	if (gamestate == GS_LEVEL)
	{
		if (mapheaderinfo[gamemap-1]->zonttl[0] && !(mapheaderinfo[gamemap-1]->levelflags & LF_NOZONE))
		{
			if (mapheaderinfo[gamemap-1]->actnum[0])
				CONS_Printf("%s (%d): %s %s %s\n", G_BuildMapName(gamemap), gamemap, mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl, mapheaderinfo[gamemap-1]->actnum);
			else
				CONS_Printf("%s (%d): %s %s\n", G_BuildMapName(gamemap), gamemap, mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->zonttl);
		}
		else
		{
			if (mapheaderinfo[gamemap-1]->actnum[0])
				CONS_Printf("%s (%d): %s %s\n", G_BuildMapName(gamemap), gamemap, mapheaderinfo[gamemap-1]->lvlttl, mapheaderinfo[gamemap-1]->actnum);
			else
				CONS_Printf("%s (%d): %s\n", G_BuildMapName(gamemap), gamemap, mapheaderinfo[gamemap-1]->lvlttl);
		}
	}
	else
		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
}

static void Command_Maphash_f(void)
{
	if (gamestate == GS_LEVEL)
		CONS_Printf("%s: " HASHFMT "\n", G_BuildMapName(gamemap), maphash);
	else
		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
}

static void Command_ExitLevel_f(void)
{
	if (!(netgame || multiplayer) && !cht_debug)
		CONS_Printf(M_GetText("This only works in a netgame.\n"));
	else if (!(server || (IsPlayerAdmin(consoleplayer))))
		CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
	else if (( gamestate != GS_LEVEL && gamestate != GS_CREDITS && gamestate != GS_BLANCREDITS && gamestate != GS_SECRETCREDITS ) || demo.playback)
		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
	else
		SendNetXCmd(XD_EXITLEVEL, NULL, 0);
}

static void Got_ExitLevelcmd(UINT8 **cp, INT32 playernum)
{
	(void)cp;

	// Ignore duplicate XD_EXITLEVEL commands.
	if (gameaction == ga_completed)
		return;

	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal exitlevel command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	G_FinishExitLevel();
}

static void Got_SetupVotecmd(UINT8 **cp, INT32 playernum)
{
	INT32 i;
	UINT8 gt, secondgt;
	INT16 tempvotelevels[12][2];

	if (playernum != serverplayer) // admin shouldn't be able to set up vote...
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal vote setup received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	// Get gametype data.
	gt = (UINT8)READUINT8(*cp);
	secondgt = (UINT8)READUINT8(*cp);

	// Strip illegal Encore flag.
	if ((gt & VOTEMODIFIER_ENCORE)
		&& !(gametypes[(gt & ~VOTEMODIFIER_ENCORE)]->rules & GTR_CIRCUIT))
	{
		gt &= ~VOTEMODIFIER_ENCORE;
	}

	if ((gt & ~VOTEMODIFIER_ENCORE) >= numgametypes)
	{
		gt &= ~VOTEMODIFIER_ENCORE;
		if (server)
			I_Error("Got_SetupVotecmd: Internal gametype ID %d not found (numgametypes = %d)", gt, numgametypes);
		CONS_Alert(CONS_WARNING, M_GetText("Vote setup with bad gametype ID %d received from %s\n"), gt, player_names[playernum]);
		return;
	}

	if ((secondgt & ~VOTEMODIFIER_ENCORE) >= numgametypes)
	{
		secondgt &= ~VOTEMODIFIER_ENCORE;
		if (server)
			I_Error("Got_SetupVotecmd: Internal second gametype ID %d not found (numgametypes = %d)", secondgt, numgametypes);
		CONS_Alert(CONS_WARNING, M_GetText("Vote setup with bad second gametype ID %d received from %s\n"), secondgt, player_names[playernum]);
		return;
	}

	for (i = 0; i < VOTEROWSADDSONE; i++)
	{
		tempvotelevels[i][0] = (UINT16)READUINT16(*cp);
		tempvotelevels[i][1] = gt;
		if (tempvotelevels[i][0] < nummapheaders && mapheaderinfo[tempvotelevels[i][0]])
			continue;

		if (server)
			I_Error("Got_SetupVotecmd: Internal map ID %d not found (nummapheaders = %d)", tempvotelevels[i][0], nummapheaders);
		CONS_Alert(CONS_WARNING, M_GetText("Vote setup with bad map ID %d received from %s\n"), tempvotelevels[i][0], player_names[playernum]);
		return;
	}

	tempvotelevels[2][1] = secondgt;

	memcpy(g_voteLevels, tempvotelevels, sizeof(g_voteLevels));

	// If third entry has an illelegal Encore flag... (illelegal!?)
	if ((secondgt & VOTEMODIFIER_ENCORE)
		&& !(gametypes[(secondgt & ~VOTEMODIFIER_ENCORE)]->rules & GTR_CIRCUIT))
	{
		secondgt &= ~VOTEMODIFIER_ENCORE;
		// Apply it to the second entry instead, gametype permitting!
		if (gametypes[gt]->rules & GTR_CIRCUIT)
		{
			g_voteLevels[1][1] |= VOTEMODIFIER_ENCORE;
		}
	}

	// Finally, set third entry's gametype/Encore status.
	g_voteLevels[2][1] = secondgt;

	G_SetGamestate(GS_VOTING);
	Y_StartVote();
}

static void Got_ModifyVotecmd(UINT8 **cp, INT32 playernum)
{
	UINT8 targetID = READUINT8(*cp);
	SINT8 vote = READSINT8(*cp);

	if (targetID >= MAXPLAYERS)
	{
		// only the server is allowed to send these
		if (playernum != serverplayer)
		{
			goto fail;
		}
	}
	else if (playeringame[targetID] == true && players[targetID].bot == true)
	{
		if (playernum != serverplayer)
		{
			goto fail;
		}
	}
	else
	{
		if (playernode[targetID] != playernode[playernum])
		{
			goto fail;
		}
	}

	g_votes[targetID] =  vote;
	return;

fail:
	CONS_Alert(CONS_WARNING,
		M_GetText ("Illegal modify vote command received from %s\n"),
		player_names[playernum]
	);

	if (server)
	{
		SendKick(playernum, KICK_MSG_CON_FAIL);
	}
}

static void Got_PickVotecmd(UINT8 **cp, INT32 playernum)
{
	SINT8 pick = READSINT8(*cp);
	SINT8 level = READSINT8(*cp);

	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal vote setup received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	Y_SetupVoteFinish(pick, level);
}

static void Got_ScheduleTaskcmd(UINT8 **cp, INT32 playernum)
{
	char command[MAXTEXTCMD];
	INT16 seconds;

	seconds = READINT16(*cp);
	READSTRING(*cp, command);

	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING,
				M_GetText ("Illegal schedule task received from %s\n"),
				player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	Schedule_Add(seconds, seconds, (const char *)command);

	if (server || consoleplayer == playernum)
	{
		CONS_Printf(
			"OK! Running \"" "\x82" "%s" "\x80" "\" every " "\x82" "%d" "\x80" " seconds.\n",
			command,
			seconds
		);
	}
}

static void Got_ScheduleClearcmd(UINT8 **cp, INT32 playernum)
{
	(void)cp;

	if (playernum != serverplayer && !IsPlayerAdmin(playernum))
	{
		CONS_Alert(CONS_WARNING,
				M_GetText ("Illegal schedule clear received from %s\n"),
				player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	Schedule_Clear();

	if (server || consoleplayer == playernum)
	{
		CONS_Printf("All scheduled tasks have been cleared.\n");
	}
}

static void Got_Automatecmd(UINT8 **cp, INT32 playernum)
{
	UINT8 eventID;
	char command[MAXTEXTCMD];

	eventID = READUINT8(*cp);
	READSTRING(*cp, command);

	if (
		(playernum != serverplayer && !IsPlayerAdmin(playernum))
		|| (eventID >= AEV__MAX)
	)
	{
		CONS_Alert(CONS_WARNING,
				M_GetText ("Illegal automate received from %s\n"),
				player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	Automate_Set(eventID, command);

	if (server || consoleplayer == playernum)
	{
		if (command[0] == '\0')
		{
			CONS_Printf(
				"Removed the %s automate command.\n",
				automate_names[eventID]
			);
		}
		else
		{
			CONS_Printf(
				"Set the %s automate command to \"" "\x82" "%s" "\x80" "\".\n",
				automate_names[eventID],
				command
			);
		}
	}
}
// Consistent print about cheaters in multiplayer.
static void CV_CheaterWarning(UINT8 playerID, const char *command)
{
	if (netgame)
	{
		CONS_Printf("\x85" "%s cheats:" "\x80" " %s\n", player_names[playerID], command);
	}
}

static void Got_Cheat(UINT8 **cp, INT32 playernum)
{
	UINT8 targetPlayer = READUINT8(*cp);
	cheat_t cheat = READUINT8(*cp);

	player_t *player;

	if (cheat >= NUMBER_OF_CHEATS || ((playernum != serverplayer) && !IsPlayerAdmin(playernum)) || targetPlayer >= MAXPLAYERS ||
			playernode[targetPlayer] != playernode[playernum])
	{
		CONS_Alert(CONS_WARNING,
				M_GetText ("Illegal cheat command received from %s\n"),
				player_names[playernum]);
		return;
	}

	player = &players[targetPlayer];

	switch (cheat)
	{
		case CHEAT_NOCLIP: {
			const char *status = "on";

			if (!P_MobjWasRemoved(player->mo))
			{
				player->mo->flags ^= MF_NOCLIP;
				player->noclip = true;

				if (!(player->mo->flags & MF_NOCLIP))
				{
					status = "off";
					player->noclip = false;
				}
			}

			CV_CheaterWarning(targetPlayer, va("noclip %s", status));
			break;
		}

		case CHEAT_GOD: {
			const char *status = (player->pflags & PF_GODMODE) ? "off" : "on";

			player->pflags ^= PF_GODMODE;

			CV_CheaterWarning(targetPlayer, va("GOD MODE %s", status));
			break;
		}

		case CHEAT_SAVECHECKPOINT: {
			fixed_t x = READFIXED(*cp);
			fixed_t y = READFIXED(*cp);
			fixed_t z = READFIXED(*cp);

			player->starpostx = x;
			player->starposty = y;
			player->starpostz = z;

			CV_CheaterWarning(targetPlayer, va("temporary checkpoint created at %d, %d, %d",
						x / FRACUNIT, y / FRACUNIT, z / FRACUNIT));
			break;
		}

		case CHEAT_RINGS: {
			SINT8 rings = READSINT8(*cp);

			// P_GivePlayerRings does value clamping
			player->rings = 0;
			P_GivePlayerRings(player, rings);

			CV_CheaterWarning(targetPlayer, va("rings = %d", rings));
			break;
		}

		case CHEAT_LIVES: {
			SINT8 lives = READSINT8(*cp);

			// P_GivePlayerLives does value clamping
			player->lives = 0;
			P_GivePlayerLives(player, lives);

			CV_CheaterWarning(targetPlayer, va("lives = %d", lives));
			break;
		}

		case CHEAT_SCALE: {
			const fixed_t smin = FRACUNIT/100;
			const fixed_t smax = 100*FRACUNIT;

			fixed_t s = READFIXED(*cp);
			float f;

			s = min(max(smin, s), smax);
			f = FIXED_TO_FLOAT(s);

			if (!P_MobjWasRemoved(player->mo))
			{
				player->mo->destscale = s;
			}

			CV_CheaterWarning(targetPlayer, va("scale = %d%s", (int)f, M_Ftrim(FIXED_TO_FLOAT(s))));
			break;
		}

		case CHEAT_FLIP: {
			if (!P_MobjWasRemoved(player->mo))
			{
				player->mo->flags2 ^= MF2_OBJECTFLIP;
			}

			CV_CheaterWarning(targetPlayer, "invert gravity");
			break;
		}

		case CHEAT_HURT: {
			INT32 damage = READINT32(*cp);

			if (!P_MobjWasRemoved(player->mo))
			{
				P_DamageMobj(player->mo, NULL, NULL, damage, DMG_NORMAL);
			}

			CV_CheaterWarning(targetPlayer, va("%d damage to me", damage));
			break;
		}

		case CHEAT_RELATIVE_TELEPORT:
		case CHEAT_TELEPORT: {
			fixed_t x = READFIXED(*cp);
			fixed_t y = READFIXED(*cp);
			fixed_t z = READFIXED(*cp);

			float f[3] = {
				FIXED_TO_FLOAT(x),
				FIXED_TO_FLOAT(y),
				FIXED_TO_FLOAT(z),
			};
			char t[3][9];

			if (!P_MobjWasRemoved(player->mo))
			{
				P_MapStart();
				if (cheat == CHEAT_RELATIVE_TELEPORT)
				{
					P_SetOrigin(player->mo,
							player->mo->x + x,
							player->mo->y + y,
							player->mo->z + z);
				}
				else
				{
					P_SetOrigin(player->mo, x, y, z);
				}
				P_MapEnd();

				player->pflags |= PF_TRUSTWAYPOINTS;
				player->bigwaypointgap = 0;

				S_StartSound(player->mo, sfx_mixup);
			}

			strlcpy(t[0], M_Ftrim(f[0]), sizeof t[0]);
			strlcpy(t[1], M_Ftrim(f[1]), sizeof t[1]);
			strlcpy(t[2], M_Ftrim(f[2]), sizeof t[2]);

			CV_CheaterWarning(targetPlayer, va("%s %d%s, %d%s, %d%s",
						cheat == CHEAT_RELATIVE_TELEPORT
						? "relative teleport by"
						: "teleport to",
						(int)f[0], t[0], (int)f[1], t[1], (int)f[2], t[2]));
			break;
		}

		case CHEAT_DEVMODE: {
			UINT32 flags = READUINT32(*cp);
			cht_debug = flags;
			CV_CheaterWarning(targetPlayer, va("devmode %x", flags));
			break;
		}

		case CHEAT_GIVEITEM: {
			UINT8 item = READUINT8(*cp);
			UINT8 amt = READUINT8(*cp);

			item = min(item, numkartitems - 1);

			K_StripItems(player);

			// Cancel roulette if rolling
			player->itemroulette = 0;

			player->itemtype = item;
			player->itemamount = amt;

			if (amt == 0)
			{
				CV_CheaterWarning(playernum, "delete my items");
			}
			else
			{
				K_AddItemRollToList((INT32)(player - players), item, amt);

				CV_CheaterWarning(playernum, va("give item %s x%d", DEH_KartItemName(item), amt));
			}
			break;
		}

		case CHEAT_SCORE: {
			UINT32 score = READUINT32(*cp);

			player->roundscore = score;

			CV_CheaterWarning(targetPlayer, va("score = %u", score));
			break;
		}

		case CHEAT_ANGLE: {
			angle_t angle = READANGLE(*cp);
			float anglef = FIXED_TO_FLOAT(AngleFixed(angle));

			P_SetPlayerAngle(player, angle);

			CV_CheaterWarning(targetPlayer, va("angle = %d%s", (int)anglef, M_Ftrim(anglef)));
			break;
		}

		case NUMBER_OF_CHEATS:
			break;
	}
}

static void Got_PlaySound(UINT8 **cp, INT32 playernum)
{
	INT32 sound_id = READINT32(*cp);

	if (playernum != serverplayer && !IsPlayerAdmin(playernum)) // hacked client, or disasterous bug
	{
		CONS_Alert(CONS_WARNING, M_GetText("Illegal playsound received from %s (serverplayer is %s)\n"), player_names[playernum], player_names[serverplayer]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	if (sound_id < 0 || sound_id >= NUMSFX)
	{
		// bad sound effect, ignore
		return;
	}

	S_StartSound(NULL, sound_id);
}

static const char *displayplayer_compose_col(int playernum)
{
	return va("\x84(%d) \x83%s\x80", playernum, player_names[playernum]);
}

static int displayplayer_col_len(const char *text)
{
	int n = strlen(text);
	int k = n;
	int i;
	for (i = 0; i < n; ++i)
	{
		if (!isprint(text[i]))
			k--;
	}
	return k;
}

static void displayplayer_calc_col(int *col, const char *text)
{
	if (text && text[0] != ' ')
	{
		int n = displayplayer_col_len(text);
		if (*col < n)
			*col = n;
	}
}

static void displayplayer_print_col(int *col, const char *text)
{
	if (text)
	{
		if (*col)
		{
			int n = *col - displayplayer_col_len(text);
			CONS_Printf("%s%*s ", text, n, "");
		}
	}
	else
		CONS_Printf("\n");
}

static void displayplayer_iter_table(int table[5], void(*col_cb)(int*,const char*))
{
	int i;

	col_cb(&table[0], "");
	for (i = 0; i < 4; ++i)
		col_cb(&table[1 + i], va(" %d", i));
	col_cb(NULL, NULL);

	col_cb(&table[0], "g_local");
	for (i = 0; i <= splitscreen; ++i)
		col_cb(&table[1 + i], displayplayer_compose_col(g_localplayers[i]));
	col_cb(NULL, NULL);

	col_cb(&table[0], "display");
	for (i = 0; i <= r_splitscreen; ++i)
		col_cb(&table[1 + i], displayplayer_compose_col(displayplayers[i]));
	col_cb(NULL, NULL);

	col_cb(&table[0], "local party");
	for (i = 0; i < G_LocalSplitscreenPartySize(consoleplayer); ++i)
		col_cb(&table[1 + i], displayplayer_compose_col(G_LocalSplitscreenPartyMember(consoleplayer, i)));
	col_cb(NULL, NULL);

	col_cb(&table[0], "final party");
	for (i = 0; i < G_PartySize(consoleplayer); ++i)
		col_cb(&table[1 + i], displayplayer_compose_col(G_PartyMember(consoleplayer, i)));
	col_cb(NULL, NULL);
}

/** Prints the number of displayplayers[0].
  */
static void Command_Displayplayer_f(void)
{
	int table[5] = {0};
	displayplayer_iter_table(table, displayplayer_calc_col);
	displayplayer_iter_table(table, displayplayer_print_col);
}

/** Quits a game and returns to the title screen.
  *
  */
void Command_ExitGame_f(void)
{
	INT32 i;

	if (dedicated)
	{
		CONS_Printf("This command cannot be used on dedicated server\n");
		return;
	}

	LUA_HookBool(false, HOOK(GameQuit));

	D_QuitNetGame();
	CL_Reset();
	CV_ClearChangedFlags();

	for (i = 0; i < MAXPLAYERS; i++)
		CL_ClearPlayer(i);

	splitscreen = 0;
	SplitScreen_OnChange();

	cht_debug = 0;
	emeralds = 0;
	memset(&luabanks, 0, sizeof(luabanks));

	if (dirmenu)
		closefilemenu(true);

	if (!modeattacking)
		D_StartTitle();
}

void Command_Retry_f(void)
{
	if (!(gamestate == GS_LEVEL || gamestate == GS_INTERMISSION))
	{
		CONS_Printf(M_GetText("You must be in a level to use this.\n"));
	}
	else if (grandprixinfo.gp == false && bossinfo.boss == false)
	{
		CONS_Printf(M_GetText("This only works in Grand Prix or Mission Mode.\n"));
	}
	else if (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE)
	{
		CONS_Printf(M_GetText("You can't retry right now!\n"));
	}
	else
	{
		M_ClearMenus(true);
		G_SetRetryFlag();
	}
}

/** Reports to the console whether or not the game has been modified.
  *
  * \todo Make it obvious, so a console command won't be necessary.
  * \sa modifiedgame
  * \author Graue <graue@oceanbase.org>
  */
static void Command_Isgamemodified_f(void)
{
	if (majormods || modifiedgame)
		CONS_Printf("The game has been modified, Record Attack data will be saved to a seperate savegame.\n");
	else
		CONS_Printf("The game has not been modified. You can play Record Attack, earn medals and unlock extras.\n");
}

static void Command_Cheats_f(void)
{
	if (COM_CheckParm("off"))
	{
		if (!(server || (IsPlayerAdmin(consoleplayer))))
			CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
		else
			CV_ResetCheatNetVars();
		return;
	}

	if (CV_CheatsEnabled())
	{
		consvar_t *cvar;
		CONS_Printf(M_GetText("At least one CHEAT-marked variable has been changed -- Cheats are enabled.\n"));

		for (cvar = consvar_vars; cvar; cvar = cvar->next)
		{
			if ((cvar->flags & CV_CHEAT) && !fastcmp(cvar->defaultvalue, cvar->string))
			{
				CONS_Printf("%s: Default Value: %s Current Value:%s\n", cvar->name, cvar->defaultvalue, cvar->string);
			}
		}

		if (server || (IsPlayerAdmin(consoleplayer)))
			CONS_Printf(M_GetText("Type CHEATS OFF to reset all cheat variables to default.\n"));
	}
	else
		CONS_Printf(M_GetText("No CHEAT-marked variables are changed -- Cheats are disabled.\n"));
}

#ifdef _DEBUG
static void Command_Togglemodified_f(void)
{
	modifiedgame = !modifiedgame;
}

static void Command_Archivetest_f(void)
{
	savebuffer_t save = {0};
	UINT32 i, wrote;
	thinker_t *th;
	if (gamestate != GS_LEVEL)
	{
		CONS_Printf("This command only works in-game, you dummy.\n");
		return;
	}

	// assign mobjnum
	i = 1;
	for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
		if (th->function != (actionf_p1)P_RemoveThinkerDelayed)
			((mobj_t *)th)->mobjnum = i++;

	// allocate buffer
	if (P_SaveBufferAlloc(&save, 1024) == false)
	{
		CONS_Printf("Unable to allocate buffer.\n");
		return;
	}

	// test archive
	CONS_Printf("LUA_Sync Archive...\n");
	save.write = true;
	LUA_Sync(&save, true, false);
	WRITEUINT8(save.p, 0x7F);
	wrote = (UINT32)(save.p - save.buffer);

	// clear Lua state, so we can really see what happens!
	CONS_Printf("Clearing state!\n");
	LUA_ClearExtVars();

	// test unarchive
	save.p = save.buffer;
	save.write = false;
	CONS_Printf("LUA_Sync Unarchive...\n");
	LUA_Sync(&save, true, false);
	i = READUINT8(save.p);
	if (i != 0x7F || wrote != (UINT32)(save.p - save.buffer))
	{
		CONS_Printf("Savegame corrupted. (write %u, read %u)\n", wrote, (UINT32)(save.p - save.buffer));
	}

	// free buffer
	P_SaveBufferFree(&save);
	CONS_Printf("Done. No crash.\n");
}
#endif

/** Give yourself an, optional quantity or one of, an item.**/

static void Command_KartGiveItem_f(void)
{
	UINT8 localplayer = g_localplayers[consoleplayer];

	int           ac;
	const char *name;
	INT32       item;

	int i;

	ac = COM_Argc();
	if (ac < 2)
	{
		CONS_Printf(
			"give <item> [amount]: Give yourself an item\n"
		);
	}
	else
	{
		item = numkartitems;

		name = COM_Argv(1);

		if (isdigit(*name) || *name == '-')
		{
			item = atoi(name);
		}
		else
		{
			/* first check exact match */
			if (!CV_CompleteValue(&cv_kartdebugitem, &name, &item))
			{
				CONS_Printf("\x83" "Autocomplete:\n");

				/* then do very loose partial matching */
				for (i = 0; i < numkartitems; i++)
				{
					if (strcasestr(DEH_KartItemName(i), name) != NULL)
					{
						CONS_Printf("\x83\t%s\n", DEH_KartItemName(i));
						item = i;
					}
				}
			}
		}

		if (item < numkartitems)
		{
			INT32 amt;

			if (ac > 2)
				amt = atoi(COM_Argv(2));
			else
				amt = (item != KITEM_NONE);/* default to one quantity, or zero, if KITEM_NONE */

			D_Cheat(localplayer, CHEAT_GIVEITEM, item, amt);
		}
		else
		{
			CONS_Alert(CONS_WARNING,
					"No item matches '%s'\n",
					name);
		}
	}
}

static void Command_Schedule_Add(void)
{
	UINT8 buf[MAXTEXTCMD];
	UINT8 *buf_p = buf;

	size_t ac;
	INT16 seconds;
	const char *command;

	if (!(server || IsPlayerAdmin(consoleplayer)))
	{
		CONS_Printf("Only the server or a remote admin can use this.\n");
		return;
	}

	ac = COM_Argc();
	if (ac < 3)
	{
		CONS_Printf("schedule <seconds> <...>: runs the specified commands on a recurring timer\n");
		return;
	}

	seconds = atoi(COM_Argv(1));

	if (seconds <= 0)
	{
		CONS_Printf("Timer must be at least 1 second.\n");
		return;
	}

	command = COM_Argv(2);

	WRITEINT16(buf_p, seconds);
	WRITESTRING(buf_p, command);

	SendNetXCmd(XD_SCHEDULETASK, buf, buf_p - buf);
}

static void Command_Schedule_Clear(void)
{
	if (!(server || IsPlayerAdmin(consoleplayer)))
	{
		CONS_Printf("Only the server or a remote admin can use this.\n");
		return;
	}

	SendNetXCmd(XD_SCHEDULECLEAR, NULL, 0);
}

static void Command_Schedule_List(void)
{
	size_t i;

	if (!(server || IsPlayerAdmin(consoleplayer)))
	{
		// I set it up in a way that this information could be available
		// to everyone, but HOSTMOD has it server/admin-only too, so eh?
		CONS_Printf("Only the server or a remote admin can use this.\n");
		return;
	}

	if (schedule_len == 0)
	{
		CONS_Printf("No tasks are scheduled.\n");
		return;
	}

	for (i = 0; i < schedule_len; i++)
	{
		scheduleTask_t *task = schedule[i];

		CONS_Printf(
			"In " "\x82" "%d" "\x80" " second%s: " "\x82" "%s" "\x80" "\n",
			task->timer,
			(task->timer > 1) ? "s" : "",
			task->command
		);
	}
}

static void Command_Automate_Set(void)
{
	UINT8 buf[MAXTEXTCMD];
	UINT8 *buf_p = buf;

	size_t ac;

	const char *event;
	size_t eventID;

	const char *command;

	if (!(server || IsPlayerAdmin(consoleplayer)))
	{
		CONS_Printf("Only the server or a remote admin can use this.\n");
		return;
	}

	ac = COM_Argc();
	if (ac < 3)
	{
		CONS_Printf("automate_set <event> <command>: sets the command to run each time a event triggers\n");
		return;
	}

	event = COM_Argv(1);

	for (eventID = 0; eventID < AEV__MAX; eventID++)
	{
		if (strcasecmp(event, automate_names[eventID]) == 0)
		{
			break;
		}
	}

	if (eventID == AEV__MAX)
	{
		CONS_Printf("Unknown event type \"%s\".\n", event);
		return;
	}

	command = COM_Argv(2);

	WRITEUINT8(buf_p, eventID);
	WRITESTRING(buf_p, command);

	SendNetXCmd(XD_AUTOMATE, buf, buf_p - buf);
}

static void Command_PlaySound(void)
{
	const char *sound;
	const size_t argc = COM_Argc();
	sfxenum_t sfx = NUMSFX;
	UINT8 buf[4];
	UINT8 *buf_p = buf;

	if (argc < 2)
	{
		CONS_Printf("playsound <name/num>: Plays a sound effect for the entire server.\n");
		return;
	}

	if (client && !IsPlayerAdmin(consoleplayer))
	{
		CONS_Printf("This can only be used by the server host.\n");
		return;
	}

	sound = COM_Argv(1);
	if (*sound >= '0' && *sound <= '9')
	{
		sfx = atoi(sound);
	}
	else
	{
		for (sfx = 0; sfx < sfxfree; sfx++)
		{
			if (S_sfx[sfx].name && fasticmp(sound, S_sfx[sfx].name))
				break;
		}
	}

	if (sfx < 0 || sfx >= NUMSFX)
	{
		CONS_Printf("Could not find sound effect named \"sfx_%s\".\n", sound);
		return;
	}

	WRITEINT32(buf_p, sfx);
	SendNetXCmd(XD_PLAYSOUND, buf, buf_p - buf);
}

/** Makes a change to ::cv_forceskin take effect immediately.
  *
  * \sa Command_SetForcedSkin_f, cv_forceskin, forcedskin
  * \author Graue <graue@oceanbase.org>
  */
static void ForceSkin_OnChange(void)
{
	// NOT in SP, silly!
	if (!(netgame || multiplayer))
		return;

	if (cv_forceskin.value < 0)
		CONS_Printf("The server has lifted the forced skin restrictions.\n");
	else
	{
		CONS_Printf("The server is restricting all players to skin \"%s\".\n",cv_forceskin.string);
		ForceAllSkins(cv_forceskin.value);
	}
}

//Allows the player's name to be changed if cv_mute is off.
static void Name_OnChange(void)
{
	if (cv_mute.value && !(server || IsPlayerAdmin(consoleplayer)))
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
		CV_StealthSet(&cv_playername[0], player_names[consoleplayer]);
	}
	else
		SendNameAndColor(0);

}

static void Name2_OnChange(void)
{
	if (cv_mute.value) //Secondary player can't be admin.
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
		CV_StealthSet(&cv_playername[1], player_names[g_localplayers[1]]);
	}
	else
		SendNameAndColor(1);
}

static void Name3_OnChange(void)
{
	if (cv_mute.value) //Third player can't be admin.
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
		CV_StealthSet(&cv_playername[2], player_names[g_localplayers[2]]);
	}
	else
		SendNameAndColor(2);
}

static void Name4_OnChange(void)
{
	if (cv_mute.value) //Secondary player can't be admin.
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
		CV_StealthSet(&cv_playername[3], player_names[g_localplayers[3]]);
	}
	else
		SendNameAndColor(3);
}

// sends the follower change for players
static void Follower_OnChange(void)
{
	char str[SKINNAMESIZE+1], cpy[SKINNAMESIZE+1];
	INT32 num;

	// there is a slight chance that we will actually use a string instead so...
	// let's investigate the string...
	strcpy(str, cv_follower[0].string);
	strcpy(cpy, cv_follower[0].string);
	strlwr(str);
	if (!fasticmp(cpy,"0") && !atoi(cpy))	// yep, that's a string alright...
	{
		if (fasticmp(cpy, "None"))
		{
			CV_StealthSet(&cv_follower[0], "-1");

			if (!Playing())
				return; // don't send anything there.

			SendNameAndColor(0);
			return;
		}

		num = K_FollowerAvailable(str);

		if (num == -1) // that's an error.
			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);

		CV_StealthSet(&cv_follower[0], str);
		cv_follower[0].value = num;
	}

	if (!Playing())
		return; // don't send anything there.

	SendNameAndColor(0);
}

// About the same as Color_OnChange but for followers.
static void Followercolor_OnChange(void)
{
	if (!Playing())
		return; // do whatever you want if you aren't in the game or don't have a follower.

	if (!P_PlayerMoving(consoleplayer))
	{
		// Color change menu scrolling fix is no longer necessary
		SendNameAndColor(0);
	}
}

// repeat for the 3 other players

static void Follower2_OnChange(void)
{
	char str[SKINNAMESIZE+1], cpy[SKINNAMESIZE+1];
	INT32 num;

	// there is a slight chance that we will actually use a string instead so...
	// let's investigate the string...
	strcpy(str, cv_follower[1].string);
	strcpy(cpy, cv_follower[1].string);
	strlwr(str);
	if (!fasticmp(cpy,"0") && !atoi(cpy))	// yep, that's a string alright...
	{
		if (fasticmp(cpy, "None"))
		{
			CV_StealthSet(&cv_follower[1], "-1");

			if (!Playing())
				return; // don't send anything there.

			SendNameAndColor(1);
			return;
		}

		num = K_FollowerAvailable(str);

		if (num == -1) // that's an error.
			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);

		CV_StealthSet(&cv_follower[1], str);
		cv_follower[1].value = num;
	}

	if (!Playing())
		return; // don't send anything there.

	SendNameAndColor(1);
}

static void Followercolor2_OnChange(void)
{
	if (!Playing())
		return; // do whatever you want if you aren't in the game or don't have a follower.

	if (!P_PlayerMoving(g_localplayers[1]))
	{
		// Color change menu scrolling fix is no longer necessary
		SendNameAndColor(1);
	}
}

static void Follower3_OnChange(void)
{
	char str[SKINNAMESIZE+1], cpy[SKINNAMESIZE+1];
	INT32 num;

	// there is a slight chance that we will actually use a string instead so...
	// let's investigate the string...
	strcpy(str, cv_follower[2].string);
	strcpy(cpy, cv_follower[2].string);
	strlwr(str);
	if (!fasticmp(cpy,"0") && !atoi(cpy))	// yep, that's a string alright...
	{
		if (fasticmp(cpy, "None"))
		{
			CV_StealthSet(&cv_follower[2], "-1");

			if (!Playing())
				return; // don't send anything there.

			SendNameAndColor(2);
			return;
		}

		num = K_FollowerAvailable(str);

		if (num == -1) // that's an error.
			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);

		CV_StealthSet(&cv_follower[2], str);
		cv_follower[2].value = num;
	}

	if (!Playing())
		return; // don't send anything there.

	SendNameAndColor(2);
}

static void Followercolor3_OnChange(void)
{
	if (!Playing())
		return; // do whatever you want if you aren't in the game or don't have a follower.

	if (!P_PlayerMoving(g_localplayers[2]))
	{
		// Color change menu scrolling fix is no longer necessary
		SendNameAndColor(2);
	}
}

static void Follower4_OnChange(void)
{
	char str[SKINNAMESIZE+1], cpy[SKINNAMESIZE+1];
	INT32 num;

	// there is a slight chance that we will actually use a string instead so...
	// let's investigate the string...
	strcpy(str, cv_follower[3].string);
	strcpy(cpy, cv_follower[3].string);
	strlwr(str);
	if (!fasticmp(cpy,"0") && !atoi(cpy))	// yep, that's a string alright...
	{
		if (fasticmp(cpy, "None"))
		{
			CV_StealthSet(&cv_follower[3], "-1");

			if (!Playing())
				return; // don't send anything there.

			SendNameAndColor(3);
			return;
		}

		num = K_FollowerAvailable(str);

		if (num == -1) // that's an error.
			CONS_Alert(CONS_WARNING, M_GetText("Follower '%s' not found\n"), str);

		CV_StealthSet(&cv_follower[3], str);
		cv_follower[3].value = num;
	}

	if (!Playing())
		return; // don't send anything there.

	SendNameAndColor(3);
}

static void Followercolor4_OnChange(void)
{
	if (!Playing())
		return; // do whatever you want if you aren't in the game or don't have a follower.

	if (!P_PlayerMoving(g_localplayers[3]))
	{
		// Color change menu scrolling fix is no longer necessary
		SendNameAndColor(3);
	}
}

/** Sends a skin change for the console player, unless that player is moving.
  * \sa cv_skin, Skin2_OnChange, Color_OnChange
  * \author Graue <graue@oceanbase.org>
  */
static void Skin_OnChange(void)
{
	Skin_FindRealNameSkin(&cv_skin[0]);

	if (!Playing())
		return; // do whatever you want

	if (!(cht_debug || devparm) && !(multiplayer || netgame) // In single player.
		&& (gamestate != GS_WAITINGPLAYERS)) // allows command line -warp x +skin y
	{
		CV_StealthSet(&cv_skin[0], skins[players[consoleplayer].skin].name);
		return;
	}

	if (CanChangeSkin(consoleplayer) && !P_PlayerMoving(consoleplayer))
		SendNameAndColor(0);
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
		CV_StealthSet(&cv_skin[0], skins[players[consoleplayer].skin].name);
	}
}

/** Sends a skin change for the secondary splitscreen player, unless that
  * player is moving.
  * \sa cv_skin2, Skin_OnChange, Color2_OnChange
  * \author Graue <graue@oceanbase.org>
  */
static void Skin2_OnChange(void)
{
	Skin_FindRealNameSkin(&cv_skin[1]);

	if (!Playing() || !splitscreen)
		return; // do whatever you want

	if (CanChangeSkin(g_localplayers[1]) && !P_PlayerMoving(g_localplayers[1]))
		SendNameAndColor(1);
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
		CV_StealthSet(&cv_skin[1], skins[players[g_localplayers[1]].skin].name);
	}
}

static void Skin3_OnChange(void)
{
	Skin_FindRealNameSkin(&cv_skin[2]);

	if (!Playing() || splitscreen < 2)
		return; // do whatever you want

	if (CanChangeSkin(g_localplayers[2]) && !P_PlayerMoving(g_localplayers[2]))
		SendNameAndColor(2);
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
		CV_StealthSet(&cv_skin[2], skins[players[g_localplayers[2]].skin].name);
	}
}

static void Skin4_OnChange(void)
{
	Skin_FindRealNameSkin(&cv_skin[3]);

	if (!Playing() || splitscreen < 3)
		return; // do whatever you want

	if (CanChangeSkin(g_localplayers[3]) && !P_PlayerMoving(g_localplayers[3]))
		SendNameAndColor(3);
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
		CV_StealthSet(&cv_skin[3], skins[players[g_localplayers[3]].skin].name);
	}
}

static void Command_ListSkins(void)
{
	int i;
	int page = 0;
	int numpages = numskins / 10 + (numskins % 10 ? 1 : 0);
	int longest_name = 0;

	if (COM_Argc() > 1)
	{
		if (sscanf(COM_Argv(1), " %d", &page) == 0)
		{
			CONS_Printf("Expected a number.\n");
			return;
		}
		else if (page > numpages)
		{
			CONS_Printf("There are only %d pages.\n", numpages);
			return;
		}
		else if (page < 0)
		{
			CONS_Printf("Page number cannot be negative.\n");
			return;
		}
		else if (page == 0)
		{
			CONS_Printf("There is no 0th page. (What are you a programmer?)\n");
			return;
		}

		--page;
	}

	CONS_Printf("Total %d skins. Page (%d/%d)\n", numskins, page + 1, numpages);

	//  Find the longest name and realname in the skins list. (used for alignment)
	for (i = page * 10; i < numskins && i < (page + 1) * 10; i++)
	{
		int length;
		skin_t *skin = &skins[i];

		if ((length = strlen(skin->name)) > longest_name)
			longest_name = length;
	}

	for (i = page * 10; i < numskins && i < (page + 1) * 10; i++)
	{
		skin_t *skin = &skins[i];
		UINT16 chatcolor = skincolors[skin->prefcolor].chatcolor;
		char color_prefix[2];

		if (chatcolor > V_TANMAP)
		{
			sprintf(color_prefix, "%c", '\x80');
		}
		else
		{
			sprintf(color_prefix, "%c", '\x80' + (chatcolor >> V_CHARCOLORSHIFT));
		}

		CONS_Printf(
			"%s[%-*s]\x80 %s\n",
			color_prefix,
			longest_name,
			skin->name,
			skin->realname);
	}

	if (page + 1 != numpages)
	{
		CONS_Printf("Use \"listskins %d\" to see the next page.\n", page + 2);
	}
	else
	{
		CONS_Printf("There are no more pages.\n");
	}
}

static void Command_ListVoices(void)
{
	int i;
	int sid = -1;
	int pid = 0;
	int page = 0;
	int longest_name = 0;
	boolean searchedskin = false;

	if (COM_Argc() > 1)
	{
		if (sscanf(COM_Argv(1), " %d", &pid) == 0)
		{
			if (fasticmp(COM_Argv(1), "--help"))
			{
				// Anyone who names their skin "--help" can suck it.
				CONS_Printf(
				    "Usage: \"listvoices <playernum/skinname> "
				    "<pagenum>\"\nAlternatively: \"listvoices\" alone\n");
				return;
			}
			// Assume the player is searching by skin name instead.
			INT32 j = R_SkinAvailable(COM_Argv(1));

			if (j == -1)
			{
				CONS_Printf(
				    "No skin exists with the name %s. \n(TIP: Type \"skin\" in the "
				    "console to see your current skin!)\n",
				    COM_Argv(1));
				return;
			}

			searchedskin = true;
			sid = j;
		}
		else if (pid > (splitscreen + 1))
		{
			CONS_Printf("There %s only %d %s.\n",
				    (splitscreen) ? "are" : "is",
				    (splitscreen + 1),
				    (splitscreen) ? "players" : "player");
			return;
		}
		else if (pid < 0)
		{
			CONS_Printf("Player number cannot be negative.\n");
			return;
		}

		if (sid == -1)
		{
			if (pid != 0)  // What the hell, sure. Let "Player 0" slide.
				pid--;

			sid = cv_skin[pid].value;
		}
	}
	else
	{
		sid = cv_skin[0].value;
	}

	if (sid == -1)
		return;

	skin_t* source_skin = &skins[sid];

	int numpages = source_skin->numvoices / 10 + (source_skin->numvoices % 10 ? 1 : 0);

	if (COM_Argc() > 2)
	{
		if (sscanf(COM_Argv(2), " %d", &page) == 0)
		{
			CONS_Printf("Expected a number.\n");
			return;
		}
		else if (page > numpages)
		{
			CONS_Printf("There %s only %d %s.\n",
				    (numpages != 1) ? "are" : "is",
				    numpages,
				    (numpages != 1) ? "pages" : "page");
			return;
		}
		else if (page < 0)
		{
			CONS_Printf("Page number cannot be negative.\n");
			return;
		}
		else if (page == 0)
		{
			CONS_Printf("There is no 0th page. (What are you a programmer?)\n");
			return;
		}

		--page;
	}

	CONS_Printf("Total %d %s. Page (%d/%d)\n",
		    source_skin->numvoices,
		    (source_skin->numvoices != 1) ? "voices" : "voice",
		    page + 1,
		    numpages);

	//  Find the longest name and realname in the voices list. (used for alignment)
	for (i = page * 10; i < source_skin->numvoices && i < (page + 1) * 10; i++)
	{
		int length;
		kartvoice_t* vox = &source_skin->voices[i];

		if ((length = strlen(vox->name)) > longest_name)
			longest_name = length;
	}

	for (i = page * 10; i < source_skin->numvoices && i < (page + 1) * 10; i++)
	{
		kartvoice_t* vox = &source_skin->voices[i];

		CONS_Printf("[%-*s]\x80 %s\n", longest_name, vox->name, vox->realname);
	}

	if (page + 1 != numpages)
	{
		if (searchedskin)
			CONS_Printf("Use \"listvoices %s %d\" to see the next page.\n",
				    source_skin->name,
				    page + 2);
		else
			CONS_Printf(
			    "Use \"listvoices %d %d\" to see the next page.\n", pid + 1, page + 2);
	}
	else
	{
		CONS_Printf("There are no more pages.\n");
	}
}

/** Sends a color change for the console player, unless that player is moving.
  * \sa cv_playercolor, Color2_OnChange, Skin_OnChange
  * \author Graue <graue@oceanbase.org>
  */
static void Color_OnChange(void)
{
	if (!Playing())
	{
		if (!cv_playercolor[0].value || !skincolors[cv_playercolor[0].value].accessible)
			CV_StealthSetValue(&cv_playercolor[0], lastgoodcolor[0]);
	}
	else
	{
		if (!(cht_debug || devparm) && !(multiplayer || netgame)) // In single player.
		{
			CV_StealthSet(&cv_skin[0], skins[players[consoleplayer].skin].name);
			return;
		}

		if (!P_PlayerMoving(consoleplayer) && skincolors[players[consoleplayer].skincolor].accessible == true)
		{
			// Color change menu scrolling fix is no longer necessary
			SendNameAndColor(0);
		}
		else
		{
			CV_StealthSetValue(&cv_playercolor[0],
				players[consoleplayer].skincolor);
		}
	}
	lastgoodcolor[0] = cv_playercolor[0].value;
}

/** Sends a color change for the secondary splitscreen player, unless that
  * player is moving.
  * \sa cv_playercolor2, Color_OnChange, Skin2_OnChange
  * \author Graue <graue@oceanbase.org>
  */
static void Color2_OnChange(void)
{
	if (!Playing() || splitscreen < 1)
	{
		if (!cv_playercolor[1].value || !skincolors[cv_playercolor[1].value].accessible)
			CV_StealthSetValue(&cv_playercolor[1], lastgoodcolor[1]);
	}
	else
	{
		if (!P_PlayerMoving(g_localplayers[1]) && skincolors[players[g_localplayers[1]].skincolor].accessible == true)
		{
			// Color change menu scrolling fix is no longer necessary
			SendNameAndColor(1);
		}
		else
		{
			CV_StealthSetValue(&cv_playercolor[1],
				players[g_localplayers[1]].skincolor);
		}
	}
	lastgoodcolor[1] = cv_playercolor[1].value;
}

static void Color3_OnChange(void)
{
	if (!Playing() || splitscreen < 2)
	{
		if (!cv_playercolor[2].value || !skincolors[cv_playercolor[2].value].accessible)
			CV_StealthSetValue(&cv_playercolor[2], lastgoodcolor[2]);
	}
	else
	{
		if (!P_PlayerMoving(g_localplayers[2]) && skincolors[players[g_localplayers[2]].skincolor].accessible == true)
		{
			// Color change menu scrolling fix is no longer necessary
			SendNameAndColor(2);
		}
		else
		{
			CV_StealthSetValue(&cv_playercolor[2],
				players[g_localplayers[2]].skincolor);
		}
	}
	lastgoodcolor[2] = cv_playercolor[2].value;
}

static void Color4_OnChange(void)
{
	if (!Playing() || splitscreen < 3)
	{
		if (!cv_playercolor[3].value || !skincolors[cv_playercolor[3].value].accessible)
			CV_StealthSetValue(&cv_playercolor[3], lastgoodcolor[3]);
	}
	else
	{
		if (!P_PlayerMoving(g_localplayers[3]) && skincolors[players[g_localplayers[3]].skincolor].accessible == true)
		{
			// Color change menu scrolling fix is no longer necessary
			SendNameAndColor(3);
		}
		else
		{
			CV_StealthSetValue(&cv_playercolor[3],
				players[g_localplayers[3]].skincolor);
		}
	}
	lastgoodcolor[3] = cv_playercolor[3].value;
}

static void __voice_cvar_func(INT32 pid, UINT8 pnum)
{
	if (!numskins)
	{
		// There aren't even any skins yet!
		return;
	}

	kartvoice_t *myvoice = P_GetMobjVoice(players[pid].mo);

	if (!myvoice)
	{
		// ...how?!
		myvoice = &skins[players[pid].skin].voices[0];
	}

	if (R_FindIDForVoice(&skins[players[pid].skin], cv_voice[pnum].string) == MAXSKINVOICES)
	{
		CONS_Alert(CONS_NOTICE, M_GetText("Voice \"%s\" does not exist for this skin.\n"), cv_voice[pnum].string);
		CV_StealthSet(&cv_voice[pnum], myvoice->name);
		return;
	}

	if (!Playing())
		return; // do whatever you want

	if (pnum > 0 && !splitscreen)
		return; // do whatever you want

	if (pnum < 1)
	{
		if (!(cht_debug || devparm) && !(multiplayer || netgame) // In single player.
			&& (gamestate != GS_WAITINGPLAYERS)) // allows command line -warp x +skin y
		{
			CV_StealthSet(&cv_voice[pnum], myvoice->name);
			return;
		}
	}

	if (!P_PlayerMoving(pid))
		SendNameAndColor(pnum);
	else
	{
		CONS_Alert(CONS_NOTICE, M_GetText("You can't change your voice at the moment.\n"));
		CV_StealthSet(&cv_voice[pnum], myvoice->name);
	}
}

static void Voice_OnChange(void)
{
	__voice_cvar_func(consoleplayer, 0);
}

static void Voice2_OnChange(void)
{
	__voice_cvar_func(g_localplayers[1], 1);
}

static void Voice3_OnChange(void)
{
	__voice_cvar_func(g_localplayers[2], 2);
}

static void Voice4_OnChange(void)
{
	__voice_cvar_func(g_localplayers[3], 3);
}

/** Displays the result of the chat being muted or unmuted.
  * The server or remote admin should already know and be able to talk
  * regardless, so this is only displayed to clients.
  *
  * \sa cv_mute
  * \author Graue <graue@oceanbase.org>
  */
static void Mute_OnChange(void)
{
	/*if (server || (IsPlayerAdmin(consoleplayer)))
		return;*/
	// Kinda dumb IMO, you should be able to see confirmation for having muted the chat as the host or admin.

	if (leveltime <= 1)
		return;	// avoid having this notification put in our console / log when we boot the server.

	if (cv_mute.value)
		HU_AddChatText(M_GetText("\x82*Chat has been muted."), false);
	else
		HU_AddChatText(M_GetText("\x82*Chat is no longer muted."), false);
}

/** Hack to clear all changed flags after game start.
  * A lot of code (written by dummies, obviously) uses COM_BufAddText() to run
  * commands and change consvars, especially on game start. This is problematic
  * because CV_ClearChangedFlags() needs to get called on game start \b after
  * all those commands are run.
  *
  * Here's how it's done: the last thing in COM_BufAddText() is "dummyconsvar
  * 1", so we end up here, where dummyconsvar is reset to 0 and all the changed
  * flags are set to 0.
  *
  * \todo Fix the aforementioned code and make this hack unnecessary.
  * \sa cv_dummyconsvar
  * \author Graue <graue@oceanbase.org>
  */
static void DummyConsvar_OnChange(void)
{
	if (cv_dummyconsvar.value == 1)
	{
		CV_SetValue(&cv_dummyconsvar, 0);
		CV_ClearChangedFlags();
	}
}

static void Command_ShowScores_f(void)
{
	UINT8 i;

	if (!(netgame || multiplayer))
	{
		CONS_Printf(M_GetText("This only works in a netgame.\n"));
		return;
	}

	for (i = 0; i < MAXPLAYERS; i++)
	{
		if (playeringame[i])
			// FIXME: %lu? what's wrong with %u? ~Callum (produces warnings...)
			CONS_Printf(M_GetText("%s's score is %u\n"), player_names[i], players[i].score);
	}
	CONS_Printf(M_GetText("The pointlimit is %d\n"), cv_pointlimit.value);

}

static void Command_ShowTime_f(void)
{
	if (!(netgame || multiplayer))
	{
		CONS_Printf(M_GetText("This only works in a netgame.\n"));
		return;
	}

	CONS_Printf(M_GetText("The current time is %f.\nThe timelimit is %f\n"), (double)leveltime/TICRATE, (double)timelimitintics/TICRATE);
}

// SRB2Kart: On change messages
static void NumLaps_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (gamestate == GS_LEVEL)
	{
		numlaps = K_RaceLapCount(gamemap - 1);

		if (cv_numlaps.value == -1)
		{
			CONS_Printf(M_GetText("Number of laps have been set to %d (map default).\n"), numlaps);
		}
		else
		{
			CONS_Printf(M_GetText("Number of laps have been set to %d.\n"), numlaps);
		}
	}
	else if (Playing())
	{
		if (cv_numlaps.value == -1)
		{
			CONS_Printf(M_GetText("Number of laps will be the map default next round.\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Number of laps will be set to %d next round.\n"), cv_numlaps.value);
		}
	}
}

static void KartFrantic_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (gamestate == GS_LEVEL && leveltime < starttime)
	{
		CONS_Printf(M_GetText("Frantic items has been set to %s.\n"), cv_kartfrantic.value ? M_GetText("on") : M_GetText("off"));
		franticitems = (boolean)cv_kartfrantic.value;
	}
	else
	{
		CONS_Printf(M_GetText("Frantic items will be turned %s next round.\n"), cv_kartfrantic.value ? M_GetText("on") : M_GetText("off"));
	}
}

static void KartSpeed_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (gamestate == GS_LEVEL && leveltime < starttime && cv_kartspeed.value != KARTSPEED_AUTO)
	{
		CONS_Printf(M_GetText("Race speed has been changed to \"%s\".\n"), cv_kartspeed.string);
		gamespeed = (UINT8)cv_kartspeed.value;
	}
	else
	{
		CONS_Printf(M_GetText("Race speed will be changed to \"%s\" next round.\n"), cv_kartspeed.string);
	}
}

static void KartBattleSpeed_OnChange(void)
{
	if (!M_SecretUnlocked(SECRET_EXPERTSPEED) && ((cv_kartbattlespeed.value == KARTSPEED_EXPERT)))
	{
		CONS_Printf(M_GetText("You haven't earned this yet.\n"));
		CV_StealthSet(&cv_kartspeed, cv_kartspeed.defaultvalue);
		return;
	}

	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (leveltime < starttime && cv_kartbattlespeed.value != KARTSPEED_AUTO)
	{
		CONS_Printf(M_GetText("Battle speed has been changed to \"%s\".\n"), cv_kartbattlespeed.string);
		gamespeed = (UINT8)cv_kartbattlespeed.value;
	}
	else
	{
		CONS_Printf(M_GetText("Battle speed will be changed to \"%s\" next round.\n"), cv_kartbattlespeed.string);
	}
}

static void KartEncore_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	CONS_Printf(M_GetText("Encore Mode will be set to %s next round.\n"), cv_kartencore.string);
}

static void KartComeback_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (leveltime < starttime)
	{
		CONS_Printf(M_GetText("Karma Comeback has been turned %s.\n"), cv_kartcomeback.value ? M_GetText("on") : M_GetText("off"));
		comeback = (boolean)cv_kartcomeback.value;
	}
	else
	{
		CONS_Printf(M_GetText("Karma Comeback will be turned %s next round.\n"), cv_kartcomeback.value ? M_GetText("on") : M_GetText("off"));
	}
}

static void KartEliminateLast_OnChange(void)
{
	P_CheckRacers();
}

static void KartRings_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_RingsActive() && cv_kartrings.value)
	{
		CONS_Printf(M_GetText("Rings will be turned \"On\" Next Round.\n"));
	}
	else if (K_RingsActive() && !cv_kartrings.value)
	{
		CONS_Printf(M_GetText("Rings will be turned \"Off\" Next Round.\n"));
	}
}

static void KartPurpleDrift_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_PurpleDriftActive() && cv_kartpurpledrift.value)
	{
		if (leveltime < starttime)
		{
			purpledriftactive = true;
			CONS_Printf(M_GetText("4-tier drifting has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("4-tier drifting will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_PurpleDriftActive() && !cv_kartpurpledrift.value)
	{
		if (leveltime < starttime)
		{
			purpledriftactive = false;
			CONS_Printf(M_GetText("4-tier drifting has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("4-tier drifting will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartStacking_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_StackingActive() && cv_kartstacking.value)
	{
		if (leveltime < starttime)
		{
			stackingactive = true;
			CONS_Printf(M_GetText("Boost Stacking has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Boost Stacking will be turned \"On\" next round.\n"));
		}
	}
	else if (K_StackingActive() && !cv_kartstacking.value)
	{
		if (leveltime < starttime)
		{
			stackingactive = false;
			CONS_Printf(M_GetText("Boost Stacking has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Boost Stacking will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartChaining_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_ChainingActive() && cv_kartchaining.value)
	{
		if (leveltime < starttime)
		{
			chainingactive = true;
			CONS_Printf(M_GetText("Boost Chaining has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Boost Chaining will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_ChainingActive() && !cv_kartchaining.value)
	{
		if (leveltime < starttime)
		{
			chainingactive = false;
			CONS_Printf(M_GetText("Boost Chaining has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Boost Chaining will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartSlipdash_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_SlipdashActive() && cv_kartslipdash.value)
	{
		if (leveltime < starttime)
		{
			slipdashactive = true;
			CONS_Printf(M_GetText("Slipdashing has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Slipdashing will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_SlipdashActive() && !cv_kartslipdash.value)
	{
		if (leveltime < starttime)
		{
			slipdashactive = false;
			CONS_Printf(M_GetText("Slipdashing has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Slipdashing will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartSlopeBoost_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_SlopeBoostActive() && cv_kartslopeboost.value)
	{
		if (leveltime < starttime)
		{
			slopeboostactive = true;
			CONS_Printf(M_GetText("Slopeboost has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Slopeboost will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_SlopeBoostActive() && !cv_kartslopeboost.value)
	{
		if (leveltime < starttime)
		{
			slopeboostactive = false;
			CONS_Printf(M_GetText("Slopeboost has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Slopeboost will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartDrafting_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_DraftingActive() && cv_kartdrafting.value)
	{
		if (leveltime < starttime)
		{
			draftingactive = true;
			CONS_Printf(M_GetText("Drafting has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Drafting will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_DraftingActive() && !cv_kartdrafting.value)
	{
		if (leveltime < starttime)
		{
			draftingactive = false;
			CONS_Printf(M_GetText("Drafting has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Drafting will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartAirDrop_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (leveltime < starttime)
	{
		CONS_Printf(M_GetText("Air Drop has been set to \"%s\".\n"), cv_kartairdrop.string);
		airdropactive = (UINT8)cv_kartairdrop.value;
	}
	else
	{
		CONS_Printf(M_GetText("Air Drop will be set to \"%s\" next round.\n"), cv_kartairdrop.string);
	}
}

static void KartAirThrust_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_AirThrustActive() && cv_kartairthrust.value)
	{
		if (leveltime < starttime)
		{
			airthrustactive = true;
			CONS_Printf(M_GetText("Air Thrust has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Air Thrust will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_AirThrustActive() && !cv_kartairthrust.value)
	{
		if (leveltime < starttime)
		{
			airthrustactive = false;
			CONS_Printf(M_GetText("Air Thrust has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Air Thrust will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartRecoveryDash_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_RecoveryDashActive() && cv_kartrecoverydash.value)
	{
		if (leveltime < starttime)
		{
			recoverydashactive = true;
			CONS_Printf(M_GetText("Recovery Dash has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Recovery Dash will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_RecoveryDashActive() && !cv_kartrecoverydash.value)
	{
		if (leveltime < starttime)
		{
			recoverydashactive = false;
			CONS_Printf(M_GetText("Recovery Dash has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Recovery Dash will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartWaterSkipLock_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_WaterskipBricksActive() && cv_kartwaterskiplock.value)
	{
		if (leveltime < starttime)
		{
			waterskipbricks = true;
			CONS_Printf(M_GetText("Waterskip Lock has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Waterskip Lock will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_WaterskipBricksActive() && !cv_kartwaterskiplock.value)
	{
		if (leveltime < starttime)
		{
			waterskipbricks = false;
			CONS_Printf(M_GetText("Waterskip Lock has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Waterskip Lock will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartItemLitter_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_ItemLitterActive() && cv_kartitemlitter.value)
	{
		if (leveltime < starttime)
		{
			itemlittering = true;
			CONS_Printf(M_GetText("Item littering has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Item littering will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_ItemLitterActive() && !cv_kartitemlitter.value)
	{
		if (leveltime < starttime)
		{
			itemlittering = false;
			CONS_Printf(M_GetText("Item littering has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Item littering will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartItemPush_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_ItemPushingActive() && cv_kartitempush.value)
	{
		if (leveltime < starttime)
		{
			itempushing = true;
			CONS_Printf(M_GetText("Item pushing has been turned \"On\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Item pushing will be turned \"On\" Next Round.\n"));
		}
	}
	else if (K_ItemPushingActive() && !cv_kartitempush.value)
	{
		if (leveltime < starttime)
		{
			itempushing = false;
			CONS_Printf(M_GetText("Item pushing has been turned \"Off\".\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Item pushing will be turned \"Off\" next round.\n"));
		}
	}
}

static void KartAntiBump_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (leveltime < starttime)
	{
		CONS_Printf(
			M_GetText("Player collision delay has been changed to %s seconds.\n"),
			cv_kartantibump.string
		);
		antibumptime = (tic_t)cv_kartantibump.value * TICRATE;
	}
	else
	{
		CONS_Printf(
			M_GetText("Player collision delay will be changed to %s seconds next round.\n"),
			cv_kartantibump.string
		);
	}
}

static void KartItemBreaker_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!multiplayer)
	{
		return;
	}

	if (!itembreaker && cv_kartitembreaker.value)
	{
		CONS_Printf(M_GetText("Singleplayer Item Breaker will be turned \"On\" Next Round.\n"));
	}
	else if (itembreaker && !cv_kartitembreaker.value)
	{
		CONS_Printf(M_GetText("Singleplayer Item Breaker will be turned \"Off\" Next Round.\n"));
	}
}

static void KartBumpSpark_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (leveltime < starttime)
	{
		CONS_Printf(M_GetText("Bump spark type has been changed to \"%s\".\n"), cv_kartbumpspark.string);
		bumpsparkactive = (UINT8)cv_kartbumpspark.value;
	}
	else
	{
		CONS_Printf(M_GetText("Bump spark type will be changed to \"%s\" next round.\n"), cv_kartbumpspark.string);
	}
}

static void KartItemList_OnChange(void)
{
	if (K_CanChangeRules(false) == false)
	{
		return;
	}

	if (!K_ItemListActive() && cv_itemlist.value)
	{
		if (leveltime < starttime)
		{
			itemlistactive = true;
			CONS_Printf(M_GetText("Item rolls will be displayed.\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Item rolls will be displayed next round.\n"));
		}
	}
	else if (K_ItemListActive() && !cv_itemlist.value)
	{
		if (leveltime < starttime)
		{
			itemlistactive = false;
			CONS_Printf(M_GetText("Item rolls will not be displayed.\n"));
		}
		else
		{
			CONS_Printf(M_GetText("Item rolls will not be displayed next round.\n"));
		}
	}
}

static void Schedule_OnChange(void)
{
	size_t i;

	if (cv_schedule.value)
	{
		return;
	}

	if (schedule_len == 0)
	{
		return;
	}

	// Reset timers when turning off.
	for (i = 0; i < schedule_len; i++)
	{
		scheduleTask_t *task = schedule[i];
		task->timer = task->basetime;
	}
}



void Got_DiscordInfo(UINT8 **p, INT32 playernum)
{
	if (playernum != serverplayer /*&& !IsPlayerAdmin(playernum)*/)
	{
		// protect against hacked/buggy client
		CONS_Alert(CONS_WARNING, M_GetText("Illegal Discord info command received from %s\n"), player_names[playernum]);
		if (server)
			SendKick(playernum, KICK_MSG_CON_FAIL);
		return;
	}

	// Don't do anything with the information if we don't have Discord RP support
#ifdef HAVE_DISCORDRPC
	discordInfo.maxPlayers = READUINT8(*p);
	discordInfo.joinsAllowed = (boolean)READUINT8(*p);
	discordInfo.everyoneCanInvite = (boolean)READUINT8(*p);

	DRPC_UpdatePresence();
#else
	(*p) += 3;
#endif
}

#undef VOTEROWS
#undef VOTEROWSADDSONE
