mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
7655 lines
177 KiB
C
7655 lines
177 KiB
C
// DR. ROBOTNIK'S RING RACERS
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) 2025 by Kart Krew.
|
|
// Copyright (C) 2020 by Sonic Team Junior.
|
|
// Copyright (C) 2000 by DooM Legacy Team.
|
|
//
|
|
// 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 "doomdef.h"
|
|
|
|
#include "console.h"
|
|
#include "command.h"
|
|
#include "i_time.h"
|
|
#include "i_system.h"
|
|
#include "g_game.h"
|
|
#include "hu_stuff.h"
|
|
#include "g_input.h"
|
|
#include "k_menu.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 "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 "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"
|
|
#include "md5.h"
|
|
|
|
// SRB2kart
|
|
#include "k_kart.h"
|
|
#include "k_battle.h"
|
|
#include "k_pwrlv.h"
|
|
#include "y_inter.h"
|
|
#include "k_color.h"
|
|
#include "k_respawn.h"
|
|
#include "k_grandprix.h"
|
|
#include "k_follower.h"
|
|
#include "doomstat.h"
|
|
#include "deh_tables.h"
|
|
#include "m_perfstats.h"
|
|
#include "k_specialstage.h"
|
|
#include "k_race.h"
|
|
#include "g_party.h"
|
|
#include "k_vote.h"
|
|
#include "k_zvote.h"
|
|
#include "k_bot.h"
|
|
#include "k_powerup.h"
|
|
#include "k_roulette.h"
|
|
#include "k_bans.h"
|
|
#include "k_director.h"
|
|
#include "k_credits.h"
|
|
#include "k_hud.h" // K_AddMessage
|
|
|
|
#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES
|
|
#include "m_avrecorder.h"
|
|
#endif
|
|
|
|
#ifdef HAVE_DISCORDRPC
|
|
#include "discord.h"
|
|
#endif
|
|
|
|
// ------
|
|
// protos
|
|
// ------
|
|
|
|
static void Got_NameAndColor(const UINT8 **cp, INT32 playernum);
|
|
static void Got_WeaponPref(const UINT8 **cp, INT32 playernum);
|
|
static void Got_PartyInvite(const UINT8 **cp, INT32 playernum);
|
|
static void Got_AcceptPartyInvite(const UINT8 **cp, INT32 playernum);
|
|
static void Got_CancelPartyInvite(const UINT8 **cp, INT32 playernum);
|
|
static void Got_LeaveParty(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Mapcmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_ExitLevelcmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_SetupVotecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_ModifyVotecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_PickVotecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_RequestAddfilecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Addfilecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Pause(const UINT8 **cp, INT32 playernum);
|
|
static void Got_RandomSeed(const UINT8 **cp, INT32 playernum);
|
|
static void Got_RunSOCcmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Spectate(const UINT8 **cp, INT32 playernum);
|
|
static void Got_TeamChange(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Setscore(const UINT8 **cp, INT32 playernum);
|
|
static void Got_DiscordInfo(const UINT8 **cp, INT32 playernum);
|
|
static void Got_ScheduleTaskcmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_ScheduleClearcmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Automatecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_MapQueuecmd(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Cheat(const UINT8 **cp, INT32 playernum);
|
|
|
|
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_StartLossless_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_QueueMap_f(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_Addfile(void);
|
|
static void Command_ListWADS_f(void);
|
|
static void Command_ListDoomednums_f(void);
|
|
static void Command_cxdiag_f(void);
|
|
static void Command_ListUnusedSprites_f(void);
|
|
static void Command_RunSOC(void);
|
|
static void Command_Pause(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_Mapmd5_f(void);
|
|
|
|
static void Command_ServerTeamChange_f(void);
|
|
|
|
static void Command_Setscore_f(void);
|
|
|
|
// Remote Administration
|
|
static void Command_Changepassword_f(void);
|
|
static void Command_Login_f(void);
|
|
static void Got_Verification(const UINT8 **cp, INT32 playernum);
|
|
static void Got_Removal(const 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(const UINT8 **cp, INT32 playernum);
|
|
|
|
static void Command_ShowScores_f(void);
|
|
static void Command_ShowTime_f(void);
|
|
|
|
static void Command_Isgamemodified_f(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_DebugMessageFeed(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_Eval(void);
|
|
|
|
static void Command_WriteTextmap(void);
|
|
static void Command_SnapshotMaps(void);
|
|
static void Command_Staffsync(void);
|
|
|
|
#ifdef DEVELOP
|
|
static void Command_FastForward(void);
|
|
#endif
|
|
|
|
// =========================================================================
|
|
// CLIENT VARIABLES
|
|
// =========================================================================
|
|
|
|
UINT16 lastgoodcolor[MAXSPLITSCREENPLAYERS] = {SKINCOLOR_NONE, SKINCOLOR_NONE, SKINCOLOR_NONE, SKINCOLOR_NONE};
|
|
|
|
CV_PossibleValue_t kartdebugitem_cons_t[] =
|
|
{
|
|
#define FOREACH( name, n ) { n, #name }
|
|
KART_ITEM_ITERATOR,
|
|
#undef FOREACH
|
|
{POWERUP_SMONITOR, "SMonitor"},
|
|
{POWERUP_BARRIER, "Barrier"},
|
|
{POWERUP_BUMPER, "Bumper"},
|
|
{POWERUP_BADGE, "Badge"},
|
|
{POWERUP_SUPERFLICKY, "SuperFlicky"},
|
|
{POWERUP_POINTS, "Points"},
|
|
{0}
|
|
};
|
|
|
|
CV_PossibleValue_t capsuletest_cons_t[] = {
|
|
{CV_CAPSULETEST_OFF, "Off"},
|
|
{CV_CAPSULETEST_MULTIPLAYER, "Multiplayer"},
|
|
{CV_CAPSULETEST_TIMEATTACK, "TimeAttack"},
|
|
{0, NULL}};
|
|
|
|
void CapsuleTest_OnChange(void);
|
|
void CapsuleTest_OnChange(void)
|
|
{
|
|
if (gamestate == GS_LEVEL)
|
|
CONS_Printf("Level must be restarted for capsuletest to have effect.\n");
|
|
}
|
|
|
|
CV_PossibleValue_t pointlimit_cons_t[] = {{1, "MIN"}, {MAXSCORE, "MAX"}, {0, "None"}, {-1, "Auto"}, {0, NULL}};
|
|
CV_PossibleValue_t numlaps_cons_t[] = {{0, "MIN"}, {MAX_LAPS, "MAX"}, {-1, "Map default"}, {0, NULL}};
|
|
|
|
CV_PossibleValue_t perfstats_cons_t[] = {
|
|
{PS_OFF, "Off"},
|
|
{PS_RENDER, "Rendering"},
|
|
{PS_LOGIC, "Logic"},
|
|
{PS_BOT, "Bots"},
|
|
{PS_THINKFRAME, "ThinkFrame"},
|
|
{0, NULL}
|
|
};
|
|
|
|
char timedemo_name[256];
|
|
boolean timedemo_csv;
|
|
char timedemo_csv_id[256];
|
|
boolean timedemo_quit;
|
|
|
|
INT16 gametype = GT_RACE;
|
|
INT16 g_lastgametype = GT_RACE;
|
|
INT16 numgametypes = GT_FIRSTFREESLOT;
|
|
|
|
boolean forceresetplayers = false;
|
|
boolean deferencoremode = false;
|
|
boolean forcespecialstage = false;
|
|
|
|
boolean staffsync = false;
|
|
|
|
UINT8 splitscreen = 0;
|
|
INT32 adminplayers[MAXPLAYERS];
|
|
|
|
// 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
|
|
"QueueStart", // AEV_QUEUESTART
|
|
"QueueEnd", // AEV_QUEUEEND
|
|
};
|
|
|
|
UINT32 livestudioaudience_timer = 90;
|
|
|
|
/// \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
|
|
"SPECTATE", // XD_SPECTATE
|
|
"SETSCORE", // XD_SETSCORE
|
|
"VERIFIED", // XD_VERIFIED
|
|
"RANDOMSEED", // XD_RANDOMSEED
|
|
"RUNSOC", // XD_RUNSOC
|
|
"REQADDFILE", // XD_REQADDFILE
|
|
"SETMOTD", // XD_SETMOTD
|
|
"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
|
|
"PARTYINVITE", // XD_PARTYINVITE
|
|
"ACCEPTPARTYINVITE", // XD_ACCEPTPARTYINVITE
|
|
"LEAVEPARTY", // XD_LEAVEPARTY
|
|
"CANCELPARTYINVITE", // XD_CANCELPARTYINVITE
|
|
"CHEAT", // XD_CHEAT
|
|
"ADDBOT", // XD_ADDBOT
|
|
"DISCORD", // XD_DISCORD
|
|
"PLAYSOUND", // XD_PLAYSOUND
|
|
"SCHEDULETASK", // XD_SCHEDULETASK
|
|
"SCHEDULECLEAR", // XD_SCHEDULECLEAR
|
|
"AUTOMATE", // XD_AUTOMATE
|
|
"",
|
|
"MAPQUEUE", // XD_MAPQUEUE
|
|
"CALLZVOTE", // XD_CALLZVOTE
|
|
"SETZVOTE", // XD_SETZVOTE
|
|
"TEAMCHANGE", // XD_TEAMCHANGE
|
|
};
|
|
|
|
// =========================================================================
|
|
// 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";
|
|
|
|
// 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_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_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_MAPQUEUE, Got_MapQueuecmd);
|
|
|
|
RegisterNetXCmd(XD_CHEAT, Got_Cheat);
|
|
|
|
// Remote Administration
|
|
COM_AddCommand("password", Command_Changepassword_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_SPECTATE, Got_Spectate);
|
|
RegisterNetXCmd(XD_TEAMCHANGE, Got_TeamChange);
|
|
COM_AddCommand("serverchangeteam", Command_ServerTeamChange_f);
|
|
|
|
RegisterNetXCmd(XD_SETSCORE, Got_Setscore);
|
|
COM_AddDebugCommand("setscore", Command_Setscore_f);
|
|
COM_AddCommand("map", Command_Map_f);
|
|
COM_AddDebugCommand("randommap", Command_RandomMap);
|
|
COM_AddCommand("restartlevel", Command_RestartLevel);
|
|
COM_AddCommand("queuemap", Command_QueueMap_f);
|
|
|
|
COM_AddCommand("exitgame", Command_ExitGame_f);
|
|
COM_AddCommand("retry", Command_Retry_f);
|
|
COM_AddCommand("exitlevel", Command_ExitLevel_f);
|
|
COM_AddDebugCommand("showmap", Command_Showmap_f);
|
|
COM_AddCommand("mapmd5", Command_Mapmd5_f);
|
|
|
|
COM_AddCommand("addfile", Command_Addfile);
|
|
COM_AddDebugCommand("listwad", Command_ListWADS_f);
|
|
COM_AddDebugCommand("listmapthings", Command_ListDoomednums_f);
|
|
COM_AddDebugCommand("cxdiag", Command_cxdiag_f);
|
|
COM_AddCommand("listunusedsprites", Command_ListUnusedSprites_f);
|
|
|
|
COM_AddCommand("runsoc", Command_RunSOC);
|
|
COM_AddCommand("pause", Command_Pause);
|
|
|
|
COM_AddDebugCommand("gametype", Command_ShowGametype_f);
|
|
COM_AddDebugCommand("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_AddDebugCommand("isgamemodified", Command_Isgamemodified_f); // test
|
|
COM_AddDebugCommand("showscores", Command_ShowScores_f);
|
|
COM_AddDebugCommand("showtime", Command_ShowTime_f);
|
|
#ifdef _DEBUG
|
|
COM_AddDebugCommand("togglemodified", Command_Togglemodified_f);
|
|
COM_AddDebugCommand("archivetest", Command_Archivetest_f);
|
|
#endif
|
|
|
|
COM_AddDebugCommand("downloads", Command_Downloads_f);
|
|
|
|
COM_AddDebugCommand("give", Command_KartGiveItem_f);
|
|
COM_AddDebugCommand("give2", Command_KartGiveItem_f);
|
|
COM_AddDebugCommand("give3", Command_KartGiveItem_f);
|
|
COM_AddDebugCommand("give4", Command_KartGiveItem_f);
|
|
|
|
COM_AddDebugCommand("debugmessagefeed", Command_DebugMessageFeed);
|
|
|
|
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);
|
|
|
|
COM_AddDebugCommand("eval", Command_Eval);
|
|
|
|
COM_AddCommand("writetextmap", Command_WriteTextmap);
|
|
|
|
COM_AddCommand("snapshotmap", Command_SnapshotMaps);
|
|
COM_AddCommand("snapshotmaps", Command_SnapshotMaps); // alias
|
|
COM_AddCommand("staffsync", Command_Staffsync);
|
|
|
|
// for master server connection
|
|
AddMServCommands();
|
|
|
|
RegisterNetXCmd(XD_RANDOMSEED, Got_RandomSeed);
|
|
|
|
// d_clisrv
|
|
|
|
COM_AddDebugCommand("ping", Command_Ping_f);
|
|
|
|
RegisterNetXCmd(XD_DISCORD, Got_DiscordInfo);
|
|
|
|
COM_AddDebugCommand("numthinkers", Command_Numthinkers_f);
|
|
COM_AddDebugCommand("countmobjs", Command_CountMobjs_f);
|
|
|
|
#ifdef _DEBUG
|
|
COM_AddDebugCommand("causecfail", Command_CauseCfail_f);
|
|
#endif
|
|
#ifdef LUA_ALLOW_BYTECODE
|
|
COM_AddCommand("dumplua", Command_Dumplua_f);
|
|
#endif
|
|
|
|
#ifdef DEVELOP
|
|
COM_AddDebugCommand("fastforward", Command_FastForward);
|
|
COM_AddDebugCommand("crypt", Command_Crypt_f);
|
|
#endif
|
|
|
|
K_RegisterMidVoteCVars();
|
|
|
|
{
|
|
extern struct CVarList *cvlist_server;
|
|
CV_RegisterList(cvlist_server);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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;
|
|
|
|
// 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("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);
|
|
|
|
COM_AddDebugCommand("resetcamera", Command_ResetCamera_f);
|
|
|
|
COM_AddDebugCommand("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("startlossless", Command_StartLossless_f);
|
|
COM_AddCommand("stopmovie", Command_StopMovie_f);
|
|
COM_AddDebugCommand("minigen", M_MinimapGenerate);
|
|
COM_AddDebugCommand("dumprrautomedaltimes", Command_dumprrautomedaltimes);
|
|
COM_AddDebugCommand("platinums", Command_Platinums);
|
|
|
|
#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES
|
|
M_AVRecorder_AddCommands();
|
|
#endif
|
|
|
|
COM_AddDebugCommand("displayplayer", Command_Displayplayer_f);
|
|
|
|
// k_menu.c
|
|
//CV_RegisterVar(&cv_compactscoreboard);
|
|
|
|
// ingame object placing
|
|
COM_AddDebugCommand("objectplace", Command_ObjectPlace_f);
|
|
//COM_AddCommand("writethings", Command_Writethings_f);
|
|
// CV_RegisterVar(&cv_grid);
|
|
// CV_RegisterVar(&cv_snapto);
|
|
|
|
// add cheats
|
|
COM_AddDebugCommand("noclip", Command_CheatNoClip_f);
|
|
COM_AddDebugCommand("god", Command_CheatGod_f);
|
|
COM_AddDebugCommand("freeze", Command_CheatFreeze_f);
|
|
COM_AddDebugCommand("setrings", Command_Setrings_f);
|
|
COM_AddDebugCommand("setspheres", Command_Setspheres_f);
|
|
COM_AddDebugCommand("setamps", Command_Setamps_f);
|
|
COM_AddDebugCommand("setlives", Command_Setlives_f);
|
|
COM_AddDebugCommand("setroundscore", Command_Setroundscore_f);
|
|
COM_AddDebugCommand("devmode", Command_Devmode_f);
|
|
COM_AddDebugCommand("savecheckpoint", Command_Savecheckpoint_f);
|
|
COM_AddDebugCommand("scale", Command_Scale_f);
|
|
COM_AddDebugCommand("gravflip", Command_Gravflip_f);
|
|
COM_AddDebugCommand("hurtme", Command_Hurtme_f);
|
|
COM_AddDebugCommand("stumble", Command_Stumble_f);
|
|
COM_AddDebugCommand("tumble", Command_Tumble_f);
|
|
COM_AddDebugCommand("whumble", Command_Whumble_f);
|
|
COM_AddDebugCommand("explode", Command_Explode_f);
|
|
COM_AddDebugCommand("spinout", Command_Spinout_f);
|
|
COM_AddDebugCommand("wipeout", Command_Wipeout_f);
|
|
COM_AddDebugCommand("sting", Command_Sting_f);
|
|
COM_AddDebugCommand("kill", Command_Kill_f);
|
|
COM_AddDebugCommand("teleport", Command_Teleport_f);
|
|
COM_AddDebugCommand("rteleport", Command_RTeleport_f);
|
|
COM_AddDebugCommand("skynum", Command_Skynum_f);
|
|
COM_AddDebugCommand("weather", Command_Weather_f);
|
|
COM_AddDebugCommand("grayscale", Command_Grayscale_f);
|
|
COM_AddDebugCommand("goto", Command_Goto_f);
|
|
COM_AddDebugCommand("angle", Command_Angle_f);
|
|
COM_AddDebugCommand("respawnat", Command_RespawnAt_f);
|
|
COM_AddDebugCommand("goto_skybox", Command_GotoSkybox_f);
|
|
|
|
{
|
|
extern struct CVarList *cvlist_player;
|
|
CV_RegisterList(cvlist_player);
|
|
}
|
|
}
|
|
|
|
/** 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 IsPlayerNameUnique(const char *name, INT32 playernum)
|
|
{
|
|
// Check if a player is currently using the name, case-insensitively.
|
|
for (INT32 ix = 0; ix < MAXPLAYERS; ix++)
|
|
{
|
|
if (ix == playernum) // Don't compare with themself.
|
|
continue;
|
|
if (playeringame[ix] == false) // This player is not ingame.
|
|
continue;
|
|
if (strcasecmp(name, player_names[ix]) == 0) // Are usernames equal?
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean IsPlayerNameGood(char *name)
|
|
{
|
|
size_t ix, len = strlen(name);
|
|
|
|
if (len == 0 || len > MAXPLAYERNAME)
|
|
return false; // Empty or too long.
|
|
if (name[0] == ' ')
|
|
return false; // Starts 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.
|
|
|
|
// Clean up trailing whitespace.
|
|
while (len && name[len-1] == ' ')
|
|
{
|
|
name[len-1] = '\0';
|
|
len--;
|
|
}
|
|
if (len == 0)
|
|
return false;
|
|
|
|
// 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; ix < len; ix++)
|
|
if (!AllowedPlayerNameChar(name[ix]))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
boolean EnsurePlayerNameIsGood(char *name, INT32 playernum)
|
|
{
|
|
size_t len = strlen(name);
|
|
|
|
// Check if a player is using a valid name.
|
|
if (!IsPlayerNameGood(name))
|
|
return false;
|
|
|
|
// Check if another player is currently using the name, case-insensitively.
|
|
if (!IsPlayerNameUnique(name, playernum))
|
|
{
|
|
// We shouldn't kick people out just because
|
|
// they joined the game with the same name
|
|
// as someone else -- modify the name instead.
|
|
|
|
// 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", 'A' + M_RandomKey(26));
|
|
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 (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);
|
|
}
|
|
}
|
|
|
|
boolean CanChangeSkin(INT32 playernum)
|
|
{
|
|
(void)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 && K_CanChangeRules(true))
|
|
return false;
|
|
|
|
// ... there used to be a lot more here, but it's now handled in Got_NameAndColor.
|
|
|
|
return true;
|
|
}
|
|
|
|
static void ForceAllSkins(INT32 forcedskin)
|
|
{
|
|
INT32 i;
|
|
|
|
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 (dedicated)
|
|
return;
|
|
|
|
// If it's me (or my brother (or my sister (or my trusty pet dog))), set appropriate skin value in cv_skin
|
|
for (i = 0; i <= splitscreen; i++)
|
|
{
|
|
if (!playeringame[g_localplayers[i]])
|
|
continue;
|
|
|
|
CV_StealthSet(&cv_skin[i], skins[forcedskin]->name);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void D_FillPlayerSkinAndColor(const UINT8 n, const player_t *player, player_config_t *config)
|
|
{
|
|
if (player != NULL)
|
|
{
|
|
// We cannot return during this function,
|
|
// handle your input sanitizing before
|
|
I_Assert(splitscreen >= n);
|
|
}
|
|
|
|
UINT16 sendColor = cv_playercolor[n].value;
|
|
UINT16 sendFollowerColor = cv_followercolor[n].value;
|
|
|
|
// don't allow inaccessible colors
|
|
if (sendColor != SKINCOLOR_NONE && K_ColorUsable(sendColor, false, true) == false)
|
|
{
|
|
if (player != NULL // in-game change
|
|
&& player->skincolor != SKINCOLOR_NONE
|
|
&& K_ColorUsable(player->skincolor, false, true) == true)
|
|
{
|
|
// Use our previous color
|
|
CV_StealthSetValue(&cv_playercolor[n], player->skincolor);
|
|
}
|
|
else
|
|
{
|
|
// Use our default color
|
|
CV_StealthSetValue(&cv_playercolor[n], SKINCOLOR_NONE);
|
|
}
|
|
|
|
sendColor = cv_playercolor[n].value;
|
|
if (player != NULL)
|
|
{
|
|
lastgoodcolor[player - players] = sendColor;
|
|
}
|
|
}
|
|
|
|
// ditto for follower colour:
|
|
if (sendFollowerColor != SKINCOLOR_NONE && K_ColorUsable(sendFollowerColor, true, true) == false)
|
|
{
|
|
CV_StealthSet(&cv_followercolor[n], "Default"); // set it to "Default". I don't care about your stupidity!
|
|
sendFollowerColor = cv_followercolor[n].value;
|
|
}
|
|
|
|
// Don't change skin if the server doesn't want you to.
|
|
if (player != NULL && !CanChangeSkin(player - players))
|
|
{
|
|
CV_StealthSet(&cv_skin[n], skins[player->skin]->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_SkinAvailableEx(cv_skin[n].string, false);
|
|
if ((cv_skin[n].value < 0) || !R_SkinUsable((player ? player - players : -1), cv_skin[n].value, false))
|
|
{
|
|
CV_StealthSet(&cv_skin[n], DEFAULTSKIN);
|
|
cv_skin[n].value = 0;
|
|
}
|
|
|
|
cv_follower[n].value = K_FollowerAvailable(cv_follower[n].string);
|
|
if (cv_follower[n].value < 0 || !K_FollowerUsable(cv_follower[n].value))
|
|
{
|
|
CV_StealthSet(&cv_follower[n], "None");
|
|
cv_follower[n].value = -1;
|
|
}
|
|
|
|
if (sendFollowerColor == SKINCOLOR_NONE)
|
|
{
|
|
if (cv_follower[n].value >= 0)
|
|
{
|
|
sendFollowerColor = followers[cv_follower[n].value].defaultcolor;
|
|
}
|
|
else
|
|
{
|
|
sendFollowerColor = SKINCOLOR_RED; // picked by dice roll, guaranteed to be random
|
|
}
|
|
}
|
|
|
|
config->skin = (UINT16)cv_skin[n].value;
|
|
config->color = sendColor;
|
|
config->follower = (horngoner ? -1 : (INT16)cv_follower[n].value);
|
|
config->follower_color = sendFollowerColor;
|
|
}
|
|
|
|
static INT32 snacpending[MAXSPLITSCREENPLAYERS] = {0,0,0,0};
|
|
static INT32 chmappending = 0;
|
|
|
|
// name, color, or skin has changed
|
|
//
|
|
static void SendNameAndColor(const UINT8 n)
|
|
{
|
|
if (!Playing())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (splitscreen < n)
|
|
{
|
|
return; // can happen if skin4/color4/name4 changed
|
|
}
|
|
|
|
const INT32 playernum = g_localplayers[n];
|
|
if (playernum == -1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
player_t *player = &players[playernum];
|
|
|
|
// Don't change name if muted
|
|
if (player_name_changes[player - players] >= MAXNAMECHANGES)
|
|
{
|
|
CV_StealthSet(&cv_playername[n], player_names[player - players]);
|
|
HU_AddChatText("\x85* You must wait to change your name again.", false);
|
|
}
|
|
else if (cv_mute.value && !(server || IsPlayerAdmin(player - players)))
|
|
{
|
|
CV_StealthSet(&cv_playername[n], player_names[player - players]);
|
|
}
|
|
else // Cleanup name if changing it
|
|
{
|
|
CleanupPlayerName(player - players, cv_playername[n].zstring);
|
|
}
|
|
|
|
player_config_t config;
|
|
D_FillPlayerSkinAndColor(n, player, &config);
|
|
|
|
UINT8 buf[MAXPLAYERNAME + 13];
|
|
UINT8 *p = buf;
|
|
|
|
WRITESTRINGN(p, cv_playername[n].zstring, MAXPLAYERNAME);
|
|
WRITEUINT16(p, config.skin);
|
|
WRITEUINT16(p, config.color);
|
|
WRITEINT16(p, config.follower);
|
|
//CONS_Printf("Sending follower id %d\n", config.follower);
|
|
WRITEUINT16(p, config.follower_color);
|
|
|
|
snacpending[n]++;
|
|
SendNetXCmdForPlayer(n, XD_NAMEANDCOLOR, buf, p - buf);
|
|
}
|
|
|
|
static void FinalisePlaystateChange(INT32 playernum)
|
|
{
|
|
demo_extradata[playernum] |= DXD_PLAYSTATE;
|
|
|
|
// Clear player score and rings if a spectator.
|
|
if (players[playernum].spectator)
|
|
{
|
|
// To attempt to discourage rage-spectators, we delay any rejoining.
|
|
// If you're engaging in a DUEL and quit early, in addition to the
|
|
// indignity of losing your PWR, you get a special extra-long delay.
|
|
if (
|
|
netgame
|
|
&& players[playernum].jointime > 1
|
|
&& players[playernum].spectatorReentry == 0
|
|
)
|
|
{
|
|
UINT8 pcount = 0;
|
|
|
|
if (cv_duelspectatorreentry.value > cv_spectatorreentry.value)
|
|
{
|
|
UINT8 i;
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
continue;
|
|
if (++pcount < 2)
|
|
continue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
players[playernum].spectatorReentry =
|
|
(pcount == 1)
|
|
? (cv_duelspectatorreentry.value * TICRATE)
|
|
: (cv_spectatorreentry.value * TICRATE);
|
|
|
|
//CONS_Printf("player %u got re-entry of %u\n", playernum, players[playernum].spectatorReentry);
|
|
}
|
|
|
|
if (gametyperules & GTR_POINTLIMIT) // SRB2kart
|
|
{
|
|
players[playernum].roundscore = 0;
|
|
K_CalculateBattleWanted();
|
|
}
|
|
|
|
K_PlayerForfeit(playernum, true);
|
|
|
|
if (players[playernum].mo)
|
|
players[playernum].mo->health = 1;
|
|
|
|
K_StripItems(&players[playernum]);
|
|
}
|
|
|
|
// Reset away view
|
|
G_ResetViews();
|
|
|
|
K_CheckBumpers(); // SRB2Kart
|
|
P_CheckRacers(); // also SRB2Kart
|
|
}
|
|
|
|
void D_PlayerChangeSkinAndColor(player_t *p, UINT16 skin, UINT16 color, INT16 follower, UINT16 followercolor)
|
|
{
|
|
const UINT16 old_color = p->prefcolor;
|
|
const UINT16 old_skin = p->prefskin;
|
|
const INT16 old_follower = p->preffollower;
|
|
const UINT16 old_follower_color = p->preffollowercolor;
|
|
|
|
// queue the rest for next round
|
|
p->prefcolor = color % numskincolors;
|
|
|
|
if (K_ColorUsable(p->prefcolor, false, false) == false)
|
|
{
|
|
p->prefcolor = SKINCOLOR_NONE;
|
|
}
|
|
|
|
p->prefskin = skin;
|
|
p->preffollowercolor = followercolor;
|
|
p->preffollower = follower;
|
|
|
|
if (
|
|
(p->jointime <= 1) // Just entered
|
|
|| (cv_restrictskinchange.value == 0 // Not restricted
|
|
&& !Y_IntermissionPlayerLock()) // Not start of intermission
|
|
)
|
|
{
|
|
// update preferences immediately
|
|
G_UpdatePlayerPreferences(p);
|
|
}
|
|
else if (P_IsMachineLocalPlayer(p) == true)
|
|
{
|
|
if (old_color != p->prefcolor
|
|
|| old_skin != p->prefskin
|
|
|| old_follower != p->preffollower
|
|
|| old_follower_color != p->preffollowercolor)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, "Your changes will take effect next match.\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void Got_NameAndColor(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
player_t *p = &players[playernum];
|
|
char name[MAXPLAYERNAME+1];
|
|
UINT16 color, followercolor;
|
|
UINT16 skin;
|
|
INT16 follower;
|
|
UINT8 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)
|
|
{
|
|
S_StartSound(NULL, sfx_monch);
|
|
I_Sleep(1000); // let the monch play out for 1 second
|
|
I_Error("snacpending[%d] negative!", i);
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
|
|
READSTRINGN(*cp, name, MAXPLAYERNAME);
|
|
skin = READUINT16(*cp);
|
|
color = READUINT16(*cp);
|
|
follower = READINT16(*cp);
|
|
followercolor = READUINT16(*cp);
|
|
|
|
// set name
|
|
if (player_name_changes[playernum] < MAXNAMECHANGES)
|
|
{
|
|
if (strcasecmp(player_names[playernum], name) != 0)
|
|
SetPlayerName(playernum, name);
|
|
}
|
|
|
|
D_PlayerChangeSkinAndColor(p, skin, color, follower, followercolor);
|
|
}
|
|
|
|
enum {
|
|
WP_KICKSTARTACCEL = 1<<0,
|
|
WP_SHRINKME = 1<<1,
|
|
WP_AUTOROULETTE = 1<<2,
|
|
WP_ANALOGSTICK = 1<<3,
|
|
WP_AUTORING = 1<<4,
|
|
WP_SELFMUTE = 1<<5,
|
|
WP_SELFDEAFEN = 1<<6,
|
|
WP_STRICTFASTFALL = 1<<7,
|
|
// WARNING: STUPID LEGACY TIMEWASTER AHEAD
|
|
// IF YOU ARE ADDING OR MODIFYING WEAPONPREFS, YOU MUST
|
|
// PRESERVE THEM IN G_PlayerReborn -- OTHERWISE THEY
|
|
// WILL MYSTERIOUSLY VANISH AFTER ONE RACE
|
|
//
|
|
// HOURS LOST TO G_PlayerReborn: UNCOUNTABLE
|
|
};
|
|
|
|
void D_FillPlayerWeaponPref(const UINT8 n, player_config_t *config)
|
|
{
|
|
UINT8 prefs = 0;
|
|
|
|
if (cv_kickstartaccel[n].value)
|
|
prefs |= WP_KICKSTARTACCEL;
|
|
|
|
if (cv_autoroulette[n].value)
|
|
prefs |= WP_AUTOROULETTE;
|
|
|
|
if (cv_shrinkme[n].value)
|
|
prefs |= WP_SHRINKME;
|
|
|
|
if (gamecontrolflags[n] & GCF_ANALOGSTICK)
|
|
prefs |= WP_ANALOGSTICK;
|
|
|
|
if (cv_autoring[n].value)
|
|
prefs |= WP_AUTORING;
|
|
|
|
if (n == 0)
|
|
{
|
|
if (cv_voice_selfmute.value)
|
|
prefs |= WP_SELFMUTE;
|
|
|
|
if (cv_voice_selfdeafen.value)
|
|
prefs |= WP_SELFDEAFEN;
|
|
}
|
|
|
|
if (cv_strictfastfall[n].value)
|
|
prefs |= WP_STRICTFASTFALL;
|
|
|
|
config->weapon_prefs = prefs;
|
|
config->min_delay = cv_mindelay.value;
|
|
}
|
|
|
|
void WeaponPref_Send(UINT8 ssplayer)
|
|
{
|
|
player_config_t config;
|
|
D_FillPlayerWeaponPref(ssplayer, &config);
|
|
|
|
UINT8 buf[2];
|
|
UINT8 *p = buf;
|
|
|
|
WRITEUINT8(p, config.weapon_prefs);
|
|
WRITEUINT8(p, config.min_delay);
|
|
|
|
SendNetXCmdForPlayer(ssplayer, XD_WEAPONPREF, buf, p - 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_AUTOROULETTE)
|
|
prefs |= WP_AUTOROULETTE;
|
|
|
|
if (player->pflags & PF_SHRINKME)
|
|
prefs |= WP_SHRINKME;
|
|
|
|
if (player->pflags & PF_ANALOGSTICK)
|
|
prefs |= WP_ANALOGSTICK;
|
|
|
|
if (player->pflags & PF_AUTORING)
|
|
prefs |= WP_AUTORING;
|
|
|
|
if (player->pflags & PF2_STRICTFASTFALL)
|
|
prefs |= WP_STRICTFASTFALL;
|
|
|
|
WRITEUINT8(*cp, prefs);
|
|
}
|
|
|
|
void WeaponPref_Set(INT32 playernum, UINT8 prefs)
|
|
{
|
|
player_t *player = &players[playernum];
|
|
|
|
player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE|PF_AUTORING);
|
|
player->pflags2 &= ~(PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_STRICTFASTFALL);
|
|
|
|
if (prefs & WP_KICKSTARTACCEL)
|
|
player->pflags |= PF_KICKSTARTACCEL;
|
|
|
|
if (prefs & WP_AUTOROULETTE)
|
|
player->pflags |= PF_AUTOROULETTE;
|
|
|
|
if (prefs & WP_SHRINKME)
|
|
player->pflags |= PF_SHRINKME;
|
|
|
|
if (prefs & WP_ANALOGSTICK)
|
|
player->pflags |= PF_ANALOGSTICK;
|
|
else
|
|
player->pflags &= ~PF_ANALOGSTICK;
|
|
|
|
if (prefs & WP_AUTORING)
|
|
player->pflags |= PF_AUTORING;
|
|
|
|
if (prefs & WP_SELFMUTE)
|
|
player->pflags2 |= PF2_SELFMUTE;
|
|
|
|
if (prefs & WP_SELFDEAFEN)
|
|
player->pflags2 |= PF2_SELFDEAFEN;
|
|
|
|
if (prefs & WP_STRICTFASTFALL)
|
|
player->pflags2 |= PF2_STRICTFASTFALL;
|
|
}
|
|
|
|
size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
|
|
{
|
|
const UINT8 *p = bufstart;
|
|
|
|
UINT8 prefs = READUINT8(p);
|
|
WeaponPref_Set(playernum, prefs);
|
|
|
|
return p - bufstart;
|
|
}
|
|
|
|
static void Got_WeaponPref(const UINT8 **cp,INT32 playernum)
|
|
{
|
|
*cp += 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;
|
|
}
|
|
|
|
// SEE ALSO g_demo.c
|
|
demo_extradata[playernum] |= DXD_WEAPONPREF;
|
|
}
|
|
|
|
static void Got_PartyInvite(const 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(const 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(const 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(const 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)
|
|
{
|
|
SendNameAndColor(n);
|
|
WeaponPref_Send(n);
|
|
}
|
|
|
|
static camera_t *LocalMutableCamera(INT32 playernum)
|
|
{
|
|
if (demo.playback)
|
|
{
|
|
// TODO: could have splitscreen support
|
|
if (!camera[0].freecam)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
return &camera[0];
|
|
}
|
|
|
|
if (G_IsPartyLocal(playernum))
|
|
{
|
|
UINT8 viewnum = G_PartyPosition(playernum);
|
|
camera_t *cam = &camera[viewnum];
|
|
|
|
if (cam->freecam || (players[playernum].spectator && !K_DirectorIsAvailable(viewnum)))
|
|
{
|
|
return cam;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void D_Cheat(INT32 playernum, INT32 cheat, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
UINT8 buf[64];
|
|
UINT8 *p = buf;
|
|
|
|
camera_t *cam;
|
|
if ((cam = LocalMutableCamera(playernum)))
|
|
{
|
|
switch (cheat)
|
|
{
|
|
case CHEAT_RELATIVE_TELEPORT:
|
|
va_start(ap, cheat);
|
|
cam->x += va_arg(ap, fixed_t);
|
|
cam->y += va_arg(ap, fixed_t);
|
|
cam->z += va_arg(ap, fixed_t);
|
|
va_end(ap);
|
|
return;
|
|
|
|
case CHEAT_TELEPORT:
|
|
va_start(ap, cheat);
|
|
cam->x = va_arg(ap, fixed_t);
|
|
cam->y = va_arg(ap, fixed_t);
|
|
cam->z = va_arg(ap, fixed_t);
|
|
va_end(ap);
|
|
return;
|
|
|
|
case CHEAT_ANGLE:
|
|
va_start(ap, cheat);
|
|
cam->angle = va_arg(ap, angle_t);
|
|
va_end(ap);
|
|
return;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!CV_CheatsEnabled())
|
|
{
|
|
CONS_Printf("This cannot be used without cheats enabled.\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_GIVEPOWERUP:
|
|
COPY(WRITEUINT8, unsigned int);
|
|
COPY(WRITEUINT16, unsigned int);
|
|
break;
|
|
|
|
case CHEAT_SCORE:
|
|
COPY(WRITEUINT32, UINT32);
|
|
break;
|
|
|
|
case CHEAT_ANGLE:
|
|
COPY(WRITEANGLE, angle_t);
|
|
break;
|
|
|
|
case CHEAT_RESPAWNAT:
|
|
COPY(WRITEINT32, INT32);
|
|
break;
|
|
|
|
case CHEAT_SPHERES:
|
|
case CHEAT_AMPS:
|
|
COPY(WRITEINT16, int);
|
|
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;
|
|
}
|
|
}
|
|
|
|
static int GetCommandViewNumber(void)
|
|
{
|
|
char c = COM_Argv(0)[strlen(COM_Argv(0))-1];/* may be digit */
|
|
|
|
switch (c)
|
|
{
|
|
default:
|
|
return 0;
|
|
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
return c - '1';
|
|
}
|
|
}
|
|
|
|
#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 = 1 + GetCommandViewNumber();
|
|
const char *playerparam;
|
|
INT32 placenum;
|
|
INT32 playernum;
|
|
INT32 firstplace, lastplace;
|
|
|
|
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)
|
|
{
|
|
const char *arg1 = NULL;
|
|
menudemo_t menudemo = {0};
|
|
|
|
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;
|
|
}
|
|
|
|
arg1 = COM_Argv(1);
|
|
|
|
// Internal if no extension, external if one exists
|
|
if (FIL_CheckExtension(arg1))
|
|
{
|
|
// External demos must be checked first
|
|
sprintf(menudemo.filepath, "%s" PATHSEP "%s", srb2home, arg1);
|
|
G_LoadDemoInfo(&menudemo, /*allownonmultiplayer*/ true);
|
|
|
|
if (menudemo.type != MD_LOADED)
|
|
{
|
|
// Do nothing because the demo can't be played
|
|
CONS_Alert(CONS_ERROR, "Unable to playdemo %s", menudemo.filepath);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
strlcpy(menudemo.filepath, arg1, sizeof(menudemo.filepath));
|
|
}
|
|
|
|
// disconnect from server here?
|
|
if (demo.playback)
|
|
G_StopDemo();
|
|
|
|
CONS_Printf(M_GetText("Playing back demo '%s'.\n"), menudemo.filepath);
|
|
|
|
demo.loadfiles = strcmp(COM_Argv(2), "-addfiles") == 0;
|
|
demo.ignorefiles = strcmp(COM_Argv(2), "-force") == 0;
|
|
|
|
G_DoPlayDemo(menudemo.filepath);
|
|
}
|
|
|
|
static void Command_Timedemo_f(void)
|
|
{
|
|
size_t i = 0;
|
|
const char *arg1 = NULL;
|
|
menudemo_t menudemo = {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;
|
|
}
|
|
|
|
arg1 = COM_Argv(1);
|
|
|
|
// Internal if no extension, external if one exists
|
|
if (FIL_CheckExtension(arg1))
|
|
{
|
|
// External demos must be checked first
|
|
sprintf(menudemo.filepath, "%s" PATHSEP "%s", srb2home, arg1);
|
|
G_LoadDemoInfo(&menudemo, /*allownonmultiplayer*/ true);
|
|
|
|
if (menudemo.type != MD_LOADED)
|
|
{
|
|
// Do nothing because the demo can't be played
|
|
CONS_Alert(CONS_ERROR, "Unable to timedemo %s", menudemo.filepath);
|
|
return;
|
|
}
|
|
|
|
strlcpy(timedemo_name, menudemo.filepath, sizeof(timedemo_name));
|
|
}
|
|
else
|
|
{
|
|
strlcpy(timedemo_name, arg1, sizeof(timedemo_name));
|
|
}
|
|
|
|
// disconnect from server here?
|
|
if (demo.playback)
|
|
G_StopDemo();
|
|
|
|
// 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(MM_AVRECORDER);
|
|
}
|
|
|
|
static void Command_StartLossless_f(void)
|
|
{
|
|
M_StartMovie(cv_lossless_recorder.value);
|
|
}
|
|
|
|
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 presetplayers 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?
|
|
* \param pforcespecialstage For certain contexts, forces a special stage.
|
|
* \sa D_GameTypeChanged, Command_Map_f
|
|
* \author Graue <graue@oceanbase.org>
|
|
*/
|
|
void D_MapChange(UINT16 mapnum, INT32 newgametype, boolean pencoremode, boolean presetplayers, INT32 delay, boolean skipprecutscene, boolean pforcespecialstage)
|
|
{
|
|
static char buf[1+1+1+1+1+2+4];
|
|
static char *buf_p = buf;
|
|
// The supplied data are assumed to be good.
|
|
I_Assert(delay >= 0 && delay <= 2);
|
|
|
|
CONS_Debug(DBG_GAMELOGIC, "Map change: mapnum=%d gametype=%d pencoremode=%d presetplayers=%d delay=%d skipprecutscene=%d pforcespecialstage = %d\n",
|
|
mapnum, newgametype, pencoremode, presetplayers, delay, skipprecutscene, pforcespecialstage);
|
|
|
|
if (delay != 2)
|
|
{
|
|
UINT8 flags = 0;
|
|
//I_Assert(W_CheckNumForName(G_BuildMapName(mapnum)) != LUMPERROR);
|
|
buf_p = buf;
|
|
if (pencoremode)
|
|
flags |= 1;
|
|
if (presetplayers)
|
|
flags |= 1<<1;
|
|
if (skipprecutscene)
|
|
flags |= 1<<2;
|
|
if (pforcespecialstage)
|
|
flags |= 1<<3;
|
|
if (roundqueue.netcommunicate)
|
|
flags |= 1<<4;
|
|
WRITEUINT8(buf_p, flags);
|
|
|
|
if (roundqueue.netcommunicate)
|
|
{
|
|
// roundqueue state
|
|
WRITEUINT8(buf_p, roundqueue.position);
|
|
WRITEUINT8(buf_p, roundqueue.size);
|
|
WRITEUINT8(buf_p, roundqueue.roundnum);
|
|
roundqueue.netcommunicate = false;
|
|
}
|
|
|
|
// new gametype value
|
|
WRITEUINT16(buf_p, newgametype);
|
|
|
|
WRITEUINT16(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(INT16 newgametype)
|
|
{
|
|
const UINT32 rules = gametypes[newgametype]->rules;
|
|
|
|
UINT8 buf[(VOTE_NUM_LEVELS * 2) + 2 + 1 + 1];
|
|
UINT8 *p = buf;
|
|
|
|
INT32 i;
|
|
|
|
UINT16 votebuffer[VOTE_NUM_LEVELS + 1];
|
|
//memset(votebuffer, UINT16_MAX, sizeof(votebuffer));
|
|
for (i = 0; i < VOTE_NUM_LEVELS + 1; i++)
|
|
{
|
|
votebuffer[i] = UINT16_MAX;
|
|
}
|
|
|
|
WRITEINT16(p, newgametype);
|
|
WRITEUINT8(p, ((cv_kartencore.value == 1) && (rules & GTR_ENCORE)));
|
|
WRITEUINT8(p, G_SometimesGetDifferentEncore());
|
|
|
|
UINT8 numPlayers = 0;
|
|
|
|
for (i = 0; i < MAXPLAYERS; ++i)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
extern consvar_t cv_forcebots; // debug
|
|
|
|
if (!(rules & GTR_BOTS) && players[i].bot && !cv_forcebots.value)
|
|
{
|
|
// Gametype doesn't support bots
|
|
continue;
|
|
}
|
|
|
|
numPlayers++;
|
|
}
|
|
|
|
for (i = 0; i < VOTE_NUM_LEVELS; i++)
|
|
{
|
|
UINT16 m = G_RandMapPerPlayerCount(
|
|
G_TOLFlag(newgametype),
|
|
prevmap, false,
|
|
(i < VOTE_NUM_LEVELS-1),
|
|
votebuffer,
|
|
numPlayers
|
|
);
|
|
votebuffer[i] = m;
|
|
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)
|
|
{
|
|
// Special game vote (map anger, duel)
|
|
if (!server)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Context value -- if context has changed, then discard vote update.
|
|
// This is to prevent votes being registered from different vote types.
|
|
// Currently used for Duel vs Normal votes.
|
|
WRITEUINT8(p, Y_VoteContext());
|
|
|
|
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(SINT8 angry_map)
|
|
{
|
|
char buf[3];
|
|
char* p = buf;
|
|
SINT8 temppicks[VOTE_TOTAL];
|
|
SINT8 templevels[VOTE_TOTAL];
|
|
UINT8 numvotes = 0, key = 0;
|
|
INT32 i;
|
|
|
|
for (i = 0; i < VOTE_TOTAL; i++)
|
|
{
|
|
if (Y_PlayerIDCanVote(i) == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (i == VOTE_SPECIAL && angry_map != VOTE_NOT_PICKED)
|
|
{
|
|
// Anger map is going to change because of
|
|
// the vote ending. We need to account for this
|
|
// here because a net command would not be ready
|
|
// in time for this code.
|
|
temppicks[numvotes] = i;
|
|
templevels[numvotes] = angry_map;
|
|
numvotes++;
|
|
continue;
|
|
}
|
|
|
|
if (g_votes[i] != VOTE_NOT_PICKED)
|
|
{
|
|
temppicks[numvotes] = i;
|
|
templevels[numvotes] = g_votes[i];
|
|
numvotes++;
|
|
}
|
|
}
|
|
|
|
if (numvotes > 0)
|
|
{
|
|
key = M_RandomKey(numvotes);
|
|
WRITESINT8(p, temppicks[key]);
|
|
WRITESINT8(p, templevels[key]);
|
|
WRITESINT8(p, angry_map);
|
|
}
|
|
else
|
|
{
|
|
WRITESINT8(p, VOTE_NOT_PICKED);
|
|
WRITESINT8(p, 0);
|
|
WRITESINT8(p, VOTE_NOT_PICKED);
|
|
}
|
|
|
|
SendNetXCmd(XD_PICKVOTE, &buf, 3);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
final = ZZ_Alloc(size);
|
|
p = final;
|
|
|
|
--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 INT32 GetGametypeParm(size_t option_gametype)
|
|
{
|
|
const char *gametypename;
|
|
INT32 newgametype;
|
|
|
|
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 -1;
|
|
}
|
|
|
|
// new gametype value
|
|
// use current one by default
|
|
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]))
|
|
{
|
|
INT16 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);
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CONS_Alert(CONS_ERROR,
|
|
"'%s' is not a valid gametype.\n",
|
|
gametypename);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (Playing() && netgame && (gametypes[newgametype]->rules & GTR_FORBIDMP))
|
|
{
|
|
CONS_Alert(CONS_ERROR,
|
|
"'%s' is not a net-compatible gametype.\n",
|
|
gametypename);
|
|
return -1;
|
|
}
|
|
|
|
return newgametype;
|
|
}
|
|
|
|
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_random;
|
|
size_t option_skill;
|
|
size_t option_server;
|
|
size_t option_match;
|
|
boolean newresetplayers;
|
|
boolean newforcespecialstage;
|
|
|
|
boolean usingcheats;
|
|
boolean ischeating;
|
|
|
|
INT32 newmapnum;
|
|
|
|
char * mapname = NULL;
|
|
char *realmapname = NULL;
|
|
|
|
INT32 newgametype = gametype;
|
|
boolean newencoremode = (cv_kartencore.value == 1);
|
|
|
|
if (Playing())
|
|
{
|
|
if (client && !IsPlayerAdmin(consoleplayer))
|
|
{
|
|
CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
|
|
return;
|
|
}
|
|
}
|
|
else if (netgame)
|
|
{
|
|
CONS_Printf(M_GetText("You cannot start a session while joining a server.\n"));
|
|
return;
|
|
}
|
|
|
|
option_force = COM_CheckPartialParm("-f");
|
|
option_gametype = COM_CheckPartialParm("-g");
|
|
option_encore = COM_CheckPartialParm("-e");
|
|
option_random = COM_CheckPartialParm("-r");
|
|
option_skill = COM_CheckParm("-skill");
|
|
option_server = COM_CheckParm("-server");
|
|
option_match = COM_CheckParm("-match");
|
|
newresetplayers = ! COM_CheckParm("-noresetplayers");
|
|
newforcespecialstage = COM_CheckParm("-forcespecialstage");
|
|
|
|
usingcheats = CV_CheatsEnabled();
|
|
ischeating = (!(netgame || multiplayer)) || (!newresetplayers) || (!K_CanChangeRules(false));
|
|
|
|
if (!( first_option = COM_FirstOption() ))
|
|
first_option = COM_Argc();
|
|
|
|
if (!option_random && first_option < 2)
|
|
{
|
|
/* I'm going over the fucking lines and I DON'T CAREEEEE */
|
|
CONS_Printf("map <name / number> [-gametype <type>] [-force] / [-random]:\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;
|
|
}
|
|
|
|
boolean getgametypefrommap = false;
|
|
|
|
// new gametype value
|
|
// use current one by default
|
|
if (option_gametype)
|
|
{
|
|
newgametype = GetGametypeParm(option_gametype);
|
|
if (newgametype == -1)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else if (option_random)
|
|
{
|
|
if (!Playing())
|
|
{
|
|
CONS_Printf("Can't use -random from the menu without -gametype.\n");
|
|
return;
|
|
}
|
|
}
|
|
else if (!Playing() || (netgame == false && grandprixinfo.gp == true))
|
|
{
|
|
getgametypefrommap = true;
|
|
}
|
|
|
|
// new encoremode value
|
|
if (option_encore)
|
|
{
|
|
newencoremode = !newencoremode;
|
|
|
|
if (!M_SecretUnlocked(SECRET_ENCORE, false) && newencoremode == true && !usingcheats)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n"));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (option_random)
|
|
{
|
|
UINT8 numPlayers = 0;
|
|
UINT16 oldmapnum = UINT16_MAX;
|
|
|
|
if (Playing())
|
|
{
|
|
UINT8 i;
|
|
for (i = 0; i < MAXPLAYERS; ++i)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
extern consvar_t cv_forcebots; // debug
|
|
|
|
if (!(gametypes[newgametype]->rules & GTR_BOTS) && players[i].bot && !cv_forcebots.value)
|
|
{
|
|
// Gametype doesn't support bots
|
|
continue;
|
|
}
|
|
|
|
numPlayers++;
|
|
}
|
|
|
|
oldmapnum = (gamestate == GS_LEVEL)
|
|
? (gamemap-1)
|
|
: prevmap;
|
|
}
|
|
|
|
newmapnum = G_RandMapPerPlayerCount(G_TOLFlag(newgametype), oldmapnum, false, false, NULL, numPlayers) + 1;
|
|
}
|
|
else
|
|
{
|
|
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 (M_MapLocked(newmapnum))
|
|
{
|
|
ischeating = true;
|
|
}
|
|
}
|
|
|
|
if (ischeating && !usingcheats)
|
|
{
|
|
CONS_Printf(M_GetText("Cheats must be enabled.\n"));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
|
|
if (getgametypefrommap)
|
|
{
|
|
if (mapheaderinfo[newmapnum-1])
|
|
{
|
|
// Let's just guess so we don't have to specify the gametype EVERY time...
|
|
newgametype = G_GuessGametypeByTOL(mapheaderinfo[newmapnum-1]->typeoflevel);
|
|
|
|
if (!option_force && newgametype == -1)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support any known gametype!\n"), realmapname, G_BuildMapName(newmapnum));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
|
|
if (newgametype == -1)
|
|
newgametype = GT_RACE; // sensible default
|
|
}
|
|
}
|
|
|
|
if (!Playing())
|
|
newresetplayers = true;
|
|
else if (!option_force && newgametype == gametype) // SRB2Kart
|
|
newresetplayers = false; // if not forcing and gametypes is the same
|
|
|
|
// don't use a gametype the map doesn't support
|
|
if (option_random || cht_debug || option_force || cv_skipmapcheck.value)
|
|
{
|
|
// The player wants us to trek on anyway. Do so.
|
|
}
|
|
else
|
|
{
|
|
// G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer
|
|
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), gametypes[newgametype]->name);
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
}
|
|
|
|
{
|
|
if ((option_match && option_server)
|
|
|| (option_match && option_skill)
|
|
|| (option_server && option_skill))
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("These options can't be combined.\nSelect only one out of -server, -match, or -skill.\n"));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
|
|
if (!Playing())
|
|
{
|
|
UINT8 ssplayers = cv_splitplayers.value-1;
|
|
boolean newnetgame = (option_server != 0);
|
|
|
|
multiplayer = true;
|
|
netgame = false;
|
|
|
|
if (cv_maxconnections.value < ssplayers+1)
|
|
CV_SetValue(&cv_maxconnections, ssplayers+1);
|
|
|
|
SV_StartSinglePlayerServer(newgametype, newnetgame);
|
|
|
|
if (splitscreen != ssplayers)
|
|
{
|
|
splitscreen = ssplayers;
|
|
SplitScreen_OnChange();
|
|
}
|
|
|
|
if (!newnetgame && (newgametype != GT_TUTORIAL) && option_match == 0)
|
|
{
|
|
grandprixinfo.gp = true;
|
|
grandprixinfo.initalize = true;
|
|
grandprixinfo.cup = NULL;
|
|
|
|
grandprixinfo.gamespeed = (cv_kartspeed.value == KARTSPEED_AUTO ? KARTSPEED_NORMAL : cv_kartspeed.value);
|
|
grandprixinfo.masterbots = false;
|
|
}
|
|
|
|
if (newnetgame)
|
|
{
|
|
restoreMenu = &PLAY_MP_OptSelectDef;
|
|
}
|
|
else
|
|
{
|
|
restoreMenu = NULL;
|
|
}
|
|
|
|
M_ClearMenus(true);
|
|
}
|
|
else if (
|
|
((grandprixinfo.gp == true ? option_match : option_skill) != 0) // Can't swap between.
|
|
|| (!netgame && (option_server != 0)) // Can't promote to server.
|
|
)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("You are already playing a game.\nReturn to the menu to use this option.\n"));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
|
|
if (grandprixinfo.gp)
|
|
{
|
|
grandprixinfo.wonround = 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_MASTER)
|
|
newskill = (INT16)num;
|
|
}
|
|
|
|
if (newskill != -1)
|
|
{
|
|
if (newskill == KARTGP_MASTER)
|
|
{
|
|
grandprixinfo.gamespeed = KARTSPEED_HARD;
|
|
grandprixinfo.masterbots = true;
|
|
}
|
|
else
|
|
{
|
|
grandprixinfo.gamespeed = newskill;
|
|
grandprixinfo.masterbots = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, newforcespecialstage);
|
|
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
}
|
|
|
|
/** 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(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 flags;
|
|
INT32 presetplayer = 1;
|
|
UINT8 skipprecutscene, pforcespecialstage;
|
|
boolean pencoremode, hasroundqueuedata;
|
|
UINT16 mapnumber, lastgametype;
|
|
|
|
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;
|
|
}
|
|
|
|
flags = READUINT8(*cp);
|
|
|
|
pencoremode = ((flags & 1) != 0);
|
|
|
|
presetplayer = ((flags & (1<<1)) != 0);
|
|
|
|
skipprecutscene = ((flags & (1<<2)) != 0);
|
|
|
|
pforcespecialstage = ((flags & (1<<3)) != 0);
|
|
|
|
hasroundqueuedata = ((flags & (1<<4)) != 0);
|
|
|
|
if (hasroundqueuedata)
|
|
{
|
|
UINT8 position = READUINT8(*cp);
|
|
UINT8 size = READUINT8(*cp);
|
|
UINT8 roundnum = READUINT8(*cp);
|
|
|
|
if (playernum != serverplayer // Clients, even admin clients, don't have full roundqueue data
|
|
|| position > size // Sanity check A (intentionally not a >= comparison)
|
|
|| size > ROUNDQUEUE_MAX) // Sanity Check B (ditto)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("Illegal round-queue data received from %s\n"), player_names[playernum]);
|
|
if (server && playernum != serverplayer)
|
|
SendKick(playernum, KICK_MSG_CON_FAIL);
|
|
return;
|
|
}
|
|
|
|
roundqueue.position = position;
|
|
if (size < roundqueue.size)
|
|
{
|
|
// Bafooliganism afoot - set to zero if the size is zero! ~toast 150523
|
|
roundqueue.size = size;
|
|
}
|
|
else while (roundqueue.size < size)
|
|
{
|
|
// We wipe rather than provide full data to prevent bloating the packet,
|
|
// and because only this data is necessary for sync. ~toast 100423
|
|
memset(&roundqueue.entries[roundqueue.size], 0, sizeof(roundentry_t));
|
|
roundqueue.size++;
|
|
}
|
|
roundqueue.roundnum = roundnum; // no sanity checking required, server is authoriative
|
|
}
|
|
|
|
// No more kicks below this line, we can now start modifying state beyond this function.
|
|
if (chmappending)
|
|
chmappending--;
|
|
|
|
lastgametype = gametype;
|
|
gametype = READUINT16(*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 (hasroundqueuedata && roundqueue.position > 0 && roundqueue.size > 0)
|
|
{
|
|
// ...we can evaluate CURRENT specifics for roundqueue data, though.
|
|
roundqueue.entries[roundqueue.position-1].gametype = gametype;
|
|
roundqueue.entries[roundqueue.position-1].encore = pencoremode;
|
|
}
|
|
|
|
if (!(gametyperules & GTR_ENCORE))
|
|
pencoremode = false;
|
|
|
|
mapnumber = READUINT16(*cp);
|
|
|
|
// Handle some Grand Prix state.
|
|
if (grandprixinfo.gp)
|
|
{
|
|
boolean caughtretry = (gametype == lastgametype
|
|
&& mapnumber == gamemap);
|
|
if (pforcespecialstage // Forced.
|
|
|| (caughtretry && grandprixinfo.eventmode == GPEVENT_SPECIAL) // Catch retries of forced.
|
|
|| (roundqueue.size == 0 && (gametyperules & (GTR_BOSS|GTR_CATCHER)))) // Force convention for the (queue)map command.
|
|
{
|
|
grandprixinfo.eventmode = GPEVENT_SPECIAL;
|
|
|
|
if (pforcespecialstage == true && gamedata->everseenspecial == false)
|
|
{
|
|
gamedata->everseenspecial = true;
|
|
// No need to do anything else here -- P_LoadLevel will get this for us!
|
|
//M_UpdateUnlockablesAndExtraEmblems(true, true);
|
|
//gamedata->deferredsave = true;
|
|
}
|
|
}
|
|
else if (gametype != GT_RACE)
|
|
{
|
|
grandprixinfo.eventmode = GPEVENT_BONUS;
|
|
}
|
|
else
|
|
{
|
|
grandprixinfo.eventmode = GPEVENT_NONE;
|
|
}
|
|
}
|
|
|
|
if (netgame)
|
|
P_ClearRandom(READUINT32(*cp));
|
|
|
|
if (!skipprecutscene)
|
|
{
|
|
DEBFILE(va("Warping to %s [resetplayer=%d lastgametype=%d gametype=%d cpnd=%d]\n",
|
|
G_BuildMapName(mapnumber), presetplayer, lastgametype, gametype, chmappending));
|
|
CON_LogMessage(M_GetText("Speeding off to level...\n"));
|
|
}
|
|
|
|
|
|
if (demo.playback && !demo.timing)
|
|
precache = false;
|
|
|
|
|
|
// Save demo in case map change happened after level finish
|
|
// (either manually with the map command, or with a redo vote)
|
|
// Isn't needed for time attack (and would also cause issues, as there
|
|
// G_RecordDemo (which sets demo.recording to true) is called before this runs)
|
|
if (demo.recording && modeattacking == ATTACKING_NONE)
|
|
G_CheckDemoStatus();
|
|
|
|
|
|
demo.willsave = (cv_recordmultiplayerdemos.value == 2);
|
|
demo.savebutton = 0;
|
|
|
|
G_InitNew(pencoremode, mapnumber, presetplayer, skipprecutscene);
|
|
if (demo.playback && !demo.timing)
|
|
precache = true;
|
|
if (demo.timing)
|
|
G_DoneLevelLoad();
|
|
|
|
#ifdef HAVE_DISCORDRPC
|
|
DRPC_UpdatePresence();
|
|
#endif
|
|
}
|
|
|
|
static void Command_RandomMap(void)
|
|
{
|
|
CONS_Printf("randommap is deprecated, please use \"map -random\" instead.\n");
|
|
}
|
|
|
|
static void Command_RestartLevel(void)
|
|
{
|
|
boolean newencore = false;
|
|
|
|
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;
|
|
}
|
|
|
|
if (K_CanChangeRules(false) == false && CV_CheatsEnabled() == false)
|
|
{
|
|
CONS_Printf(M_GetText("Cheats must be enabled.\n"));
|
|
return;
|
|
}
|
|
|
|
if (cv_kartencore.value != 0)
|
|
{
|
|
newencore = (cv_kartencore.value == 1) || encoremode;
|
|
}
|
|
|
|
D_MapChange(gamemap, g_lastgametype, newencore, false, 0, false, false);
|
|
}
|
|
|
|
void Handle_MapQueueSend(UINT16 newmapnum, UINT16 newgametype, boolean newencoremode)
|
|
{
|
|
UINT8 flags = 0;
|
|
|
|
CONS_Debug(DBG_GAMELOGIC, "Map queue: mapnum=%d newgametype=%d newencoremode=%d\n",
|
|
newmapnum, newgametype, newencoremode);
|
|
|
|
if (newencoremode)
|
|
flags |= 1;
|
|
|
|
netbuffer->packettype = PT_REQMAPQUEUE;
|
|
|
|
reqmapqueue_pak *reqmapqueue = &netbuffer->u.reqmapqueue;
|
|
|
|
reqmapqueue->newmapnum = newmapnum;
|
|
reqmapqueue->flags = flags;
|
|
reqmapqueue->newgametype = newgametype;
|
|
reqmapqueue->source = consoleplayer;
|
|
|
|
HSendPacket(servernode, false, 0, sizeof(reqmapqueue_pak));
|
|
|
|
// See PT_ReqMapQueue and Got_MapQueuecmd for the next stages
|
|
}
|
|
|
|
static void Command_QueueMap_f(void)
|
|
{
|
|
size_t first_option;
|
|
size_t option_force;
|
|
size_t option_gametype;
|
|
size_t option_encore;
|
|
size_t option_clear;
|
|
size_t option_show;
|
|
size_t option_random;
|
|
|
|
boolean usingcheats;
|
|
boolean ischeating;
|
|
|
|
INT32 newmapnum;
|
|
|
|
char * mapname = NULL;
|
|
char *realmapname = NULL;
|
|
|
|
INT32 newgametype = gametype;
|
|
boolean newencoremode = (cv_kartencore.value == 1);
|
|
|
|
if (!Playing())
|
|
{
|
|
CONS_Printf(M_GetText("Levels can only be queued in-game.\n"));
|
|
return;
|
|
}
|
|
|
|
if (client && !IsPlayerAdmin(consoleplayer))
|
|
{
|
|
CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
|
|
return;
|
|
}
|
|
|
|
usingcheats = CV_CheatsEnabled();
|
|
ischeating = (!(netgame || multiplayer) || !K_CanChangeRules(false));
|
|
|
|
// Early check, rather than multiple copypaste.
|
|
if (ischeating && !usingcheats)
|
|
{
|
|
CONS_Printf(M_GetText("Cheats must be enabled.\n"));
|
|
return;
|
|
}
|
|
|
|
option_clear = COM_CheckPartialParm("-c");
|
|
|
|
if (option_clear)
|
|
{
|
|
if (roundqueue.size == 0)
|
|
{
|
|
CONS_Printf(M_GetText("Round queue is already empty!\n"));
|
|
return;
|
|
}
|
|
|
|
Handle_MapQueueSend(0, ROUNDQUEUE_CMD_CLEAR, false);
|
|
return;
|
|
}
|
|
|
|
option_show = COM_CheckPartialParm("-s");
|
|
|
|
if (option_show)
|
|
{
|
|
Handle_MapQueueSend(0, ROUNDQUEUE_CMD_SHOW, false);
|
|
return;
|
|
}
|
|
|
|
if (roundqueue.size >= ROUNDQUEUE_MAX)
|
|
{
|
|
CONS_Printf(M_GetText("Round queue is currently full.\n"));
|
|
return;
|
|
}
|
|
|
|
option_force = COM_CheckPartialParm("-f");
|
|
option_gametype = COM_CheckPartialParm("-g");
|
|
option_encore = COM_CheckPartialParm("-e");
|
|
option_random = COM_CheckPartialParm("-r");
|
|
|
|
if (!( first_option = COM_FirstOption() ))
|
|
first_option = COM_Argc();
|
|
|
|
if (!option_random && first_option < 2)
|
|
{
|
|
/* I'm going over the fucking lines and I DON'T CAREEEEE */
|
|
CONS_Printf("queuemap <name / number> [-gametype <type>] [-force] / [-random] / [-clear] / [-show]:\n");
|
|
CONS_Printf(M_GetText(
|
|
"Queue up a map by its name, or by its number (though why would you).\n"
|
|
"All parameters are case-insensitive and may be abbreviated.\n"));
|
|
return;
|
|
}
|
|
|
|
// new gametype value
|
|
// use current one by default
|
|
if (option_gametype)
|
|
{
|
|
newgametype = GetGametypeParm(option_gametype);
|
|
if (newgametype == -1)
|
|
{
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// new encoremode value
|
|
if (option_encore)
|
|
{
|
|
newencoremode = !newencoremode;
|
|
|
|
if (!M_SecretUnlocked(SECRET_ENCORE, false) && newencoremode == true && !usingcheats)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n"));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (option_random)
|
|
{
|
|
// Unlike map -random, this is a server side RNG roll
|
|
newmapnum = NEXTMAP_VOTING + 1;
|
|
}
|
|
else
|
|
{
|
|
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 (M_MapLocked(newmapnum))
|
|
{
|
|
ischeating = true;
|
|
}
|
|
}
|
|
|
|
if (ischeating && !usingcheats)
|
|
{
|
|
CONS_Printf(M_GetText("Cheats must be enabled.\n"));
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
|
|
// don't use a gametype the map doesn't support
|
|
if (option_random || cht_debug || option_force || cv_skipmapcheck.value)
|
|
{
|
|
// The player wants us to trek on anyway. Do so.
|
|
}
|
|
else
|
|
{
|
|
// G_TOLFlag handles both multiplayer gametype and ignores it for !multiplayer
|
|
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), gametypes[newgametype]->name);
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
}
|
|
|
|
Handle_MapQueueSend(newmapnum-1, newgametype, newencoremode);
|
|
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
}
|
|
|
|
static void Got_MapQueuecmd(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 flags, queueposition, i;
|
|
boolean setencore;
|
|
UINT16 setgametype;
|
|
boolean doclear = false;
|
|
|
|
flags = READUINT8(*cp);
|
|
|
|
setencore = ((flags & 1) != 0);
|
|
|
|
setgametype = READUINT16(*cp);
|
|
|
|
queueposition = READUINT8(*cp);
|
|
|
|
if (playernum != serverplayer)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("Illegal map queue command received from %s\n"), player_names[playernum]);
|
|
if (server)
|
|
SendKick(playernum, KICK_MSG_CON_FAIL);
|
|
return;
|
|
}
|
|
|
|
doclear = (setgametype == ROUNDQUEUE_CMD_CLEAR);
|
|
|
|
if (doclear == false && queueposition >= ROUNDQUEUE_MAX)
|
|
{
|
|
CONS_Alert(CONS_ERROR, "queuemap: Unable to add map beyond %u\n", roundqueue.size);
|
|
return;
|
|
}
|
|
|
|
if (!server)
|
|
{
|
|
if (doclear == true)
|
|
{
|
|
memset(&roundqueue, 0, sizeof(struct roundqueue));
|
|
}
|
|
else
|
|
{
|
|
while (roundqueue.size <= queueposition)
|
|
{
|
|
memset(&roundqueue.entries[roundqueue.size], 0, sizeof(roundentry_t));
|
|
roundqueue.size++;
|
|
}
|
|
|
|
G_MapSlipIntoRoundQueue(queueposition, 0, setgametype, setencore, false);
|
|
}
|
|
|
|
for (i = 0; i <= splitscreen; i++)
|
|
{
|
|
if (!IsPlayerAdmin(g_localplayers[i]))
|
|
continue;
|
|
break;
|
|
}
|
|
|
|
if (i > splitscreen)
|
|
return;
|
|
}
|
|
|
|
if (doclear)
|
|
CONS_Printf("queuemap: The round queue was cleared.\n");
|
|
else
|
|
CONS_Printf("queuemap: A map was added to the round queue (pos. %u)\n", queueposition+1);
|
|
}
|
|
|
|
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;
|
|
}
|
|
// TODO: this would make a great debug feature for release
|
|
#ifndef DEVELOP
|
|
else if (modeattacking) // in time attack, pausing restarts the map
|
|
{
|
|
//M_ModeAttackRetry(0); // directly call from m_menu;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
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(const 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;
|
|
}
|
|
|
|
// TODO: this would make a great debug feature for release
|
|
#ifndef DEVELOP
|
|
if (modeattacking && !demo.playback)
|
|
return;
|
|
#endif
|
|
|
|
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 (!menuactive || netgame)
|
|
S_PauseAudio();
|
|
}
|
|
else
|
|
S_ResumeAudio();
|
|
}
|
|
|
|
I_UpdateMouseGrab();
|
|
G_ResetAllDeviceRumbles();
|
|
}
|
|
|
|
/** 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(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT32 seed;
|
|
|
|
seed = READUINT32(*cp);
|
|
|
|
if (playernum != serverplayer) // it's not from the server, wtf?
|
|
return;
|
|
|
|
// Sal: this seems unused, so this is probably fine?
|
|
// If it needs specific RNG classes, then it should be easy to add.
|
|
P_ClearRandom(seed);
|
|
}
|
|
|
|
/** Clears all players' scores in a netgame.
|
|
* Only the server or a remote admin can use this command, for obvious reasons.
|
|
*
|
|
* \sa XD_SETSCORE, Got_Setscore
|
|
* \author SSNTails <http://www.ssntails.org>
|
|
*/
|
|
static void Command_Setscore_f(void)
|
|
{
|
|
size_t option_add;
|
|
size_t option_clear;
|
|
|
|
UINT8 edit_player = UINT8_MAX;
|
|
UINT32 desired_score = 0;
|
|
|
|
UINT8 buf[1+4];
|
|
UINT8 *p = buf;
|
|
|
|
if (!Playing())
|
|
{
|
|
CONS_Printf(M_GetText("Scores can only be updated in-game.\n"));
|
|
return;
|
|
}
|
|
|
|
if (client && !IsPlayerAdmin(consoleplayer))
|
|
{
|
|
CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
|
|
return;
|
|
}
|
|
|
|
if (K_UsingPowerLevels() != PWRLV_DISABLED)
|
|
{
|
|
CONS_Printf("PWR is currently active - setscore has no effect.\n");
|
|
return;
|
|
}
|
|
|
|
if (!K_CanChangeRules(false) && !CV_CheatsEnabled())
|
|
{
|
|
CONS_Printf(M_GetText("Cheats must be enabled.\n"));
|
|
return;
|
|
}
|
|
|
|
option_clear = COM_CheckPartialParm("-c");
|
|
|
|
if (option_clear)
|
|
{
|
|
WRITEUINT8(p, edit_player);
|
|
WRITEUINT32(p, desired_score);
|
|
|
|
SendNetXCmd(XD_SETSCORE, &buf, p - buf);
|
|
return;
|
|
}
|
|
|
|
option_add = COM_CheckPartialParm("-a");
|
|
|
|
if (COM_Argc() < (option_add ? 3 : 2))
|
|
{
|
|
CONS_Printf("setscore <playernum> <value> [-add] / [-clear]\n");
|
|
return;
|
|
}
|
|
|
|
size_t work_option = 0;
|
|
if (++work_option == option_add)
|
|
work_option++;
|
|
|
|
{
|
|
const char *pid_string = COM_Argv(work_option);
|
|
INT32 pid = atoi(pid_string);
|
|
|
|
if (pid >= MAXPLAYERS
|
|
|| pid < 0
|
|
|| isdigit(pid_string[0]) == false)
|
|
{
|
|
CONS_Printf("playernum must be between 0 and %u\n", MAXPLAYERS-1);
|
|
return;
|
|
}
|
|
|
|
edit_player = pid;
|
|
}
|
|
|
|
if (++work_option == option_add)
|
|
work_option++;
|
|
|
|
{
|
|
const char *score_string = COM_Argv(work_option);
|
|
INT32 score = atoi(score_string);
|
|
INT32 min_score = (option_add ? -MAXSCORE : 0);
|
|
|
|
if (score >= MAXSCORE
|
|
|| score < min_score
|
|
|| isdigit(score_string[0]) == false)
|
|
{
|
|
CONS_Printf("score%s must be between %d and %u\n", (option_add ? " -add" : ""), min_score, MAXSCORE);
|
|
return;
|
|
}
|
|
|
|
if (option_add)
|
|
{
|
|
score += (INT32)players[edit_player].score;
|
|
}
|
|
|
|
if (score > MAXSCORE)
|
|
{
|
|
score = MAXSCORE;
|
|
}
|
|
else if (score < 0)
|
|
{
|
|
score = 0;
|
|
}
|
|
|
|
desired_score = score;
|
|
}
|
|
|
|
if (edit_player > MAXPLAYERS)
|
|
return;
|
|
|
|
if (!playeringame[edit_player])
|
|
{
|
|
CONS_Printf("playernum must be a valid player\n");
|
|
return;
|
|
}
|
|
|
|
if (players[edit_player].spectator)
|
|
{
|
|
CONS_Printf("playernum must not be a spectator\n");
|
|
return;
|
|
}
|
|
|
|
WRITEUINT8(p, edit_player);
|
|
WRITEUINT32(p, desired_score);
|
|
|
|
SendNetXCmd(XD_SETSCORE, &buf, p - buf);
|
|
}
|
|
|
|
/** Handles an ::XD_SETSCORE message, which sets one (or all) player's score(s)
|
|
*
|
|
* \param cp Data buffer.
|
|
* \param playernum Player responsible for the message. Must be ::serverplayer
|
|
* or ::adminplayer.
|
|
* \sa XD_SETSCORE, Command_Setscore_f
|
|
* \author SSNTails <http://www.ssntails.org>
|
|
*/
|
|
static void Got_Setscore(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 edit_player = READUINT8(*cp);
|
|
UINT32 desired_score = READUINT32(*cp);
|
|
|
|
if (playernum != serverplayer && !IsPlayerAdmin(playernum))
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("Illegal setscore command received from %s\n"), player_names[playernum]);
|
|
if (server)
|
|
SendKick(playernum, KICK_MSG_CON_FAIL);
|
|
return;
|
|
}
|
|
|
|
if (K_UsingPowerLevels() != PWRLV_DISABLED)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (edit_player == UINT8_MAX)
|
|
{
|
|
for (edit_player = 0; edit_player < MAXPLAYERS; edit_player++)
|
|
{
|
|
if (!playeringame[edit_player])
|
|
continue;
|
|
|
|
players[edit_player].score = 0;
|
|
}
|
|
|
|
HU_AddChatText("\x82*All scores have been reset.", false);
|
|
|
|
return;
|
|
}
|
|
|
|
if (edit_player >= MAXPLAYERS || !playeringame[edit_player])
|
|
{
|
|
CONS_Printf("Invalid player ID %u recieved a score set!\n", edit_player);
|
|
return;
|
|
}
|
|
|
|
if (players[edit_player].spectator)
|
|
{
|
|
CONS_Printf("%s recieved a score set but is a spectator!\n", player_names[edit_player]);
|
|
return;
|
|
}
|
|
|
|
players[edit_player].score = desired_score;
|
|
|
|
HU_AddChatText(va("\x82*%s had their score set to %u.", player_names[edit_player], desired_score), false);
|
|
|
|
if (server)
|
|
CONS_Printf("This setscore was done by %s (Player %u).\n", player_names[playernum], playernum);
|
|
}
|
|
|
|
static void Command_ServerTeamChange_f(void)
|
|
{
|
|
UINT8 buf[2];
|
|
UINT8 *p = buf;
|
|
|
|
UINT8 new_team = TEAM_UNASSIGNED;
|
|
UINT8 player_num = consoleplayer;
|
|
|
|
if (!(server || (IsPlayerAdmin(consoleplayer))))
|
|
{
|
|
CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
|
|
return;
|
|
}
|
|
|
|
if (G_GametypeHasTeams() == false)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("This command cannot be used currently.\n"));
|
|
return;
|
|
}
|
|
|
|
// 0 1 2
|
|
// serverchangeteam <playernum> <team>
|
|
|
|
if (COM_Argc() < 3)
|
|
{
|
|
CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "orange, blue, or auto");
|
|
return;
|
|
}
|
|
|
|
if (!strcasecmp(COM_Argv(2), "orange") || !strcasecmp(COM_Argv(2), "1"))
|
|
{
|
|
new_team = TEAM_ORANGE;
|
|
}
|
|
else if (!strcasecmp(COM_Argv(2), "blue") || !strcasecmp(COM_Argv(2), "2"))
|
|
{
|
|
new_team = TEAM_BLUE;
|
|
}
|
|
else if (!strcasecmp(COM_Argv(2), "auto") || !strcasecmp(COM_Argv(2), "0"))
|
|
{
|
|
new_team = TEAM_UNASSIGNED;
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf(M_GetText("serverchangeteam <playernum> <team>: switch player to a new team (%s)\n"), "orange, blue, or auto");
|
|
return;
|
|
}
|
|
|
|
player_num = atoi(COM_Argv(1));
|
|
|
|
if (playeringame[player_num] == false)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("There is no player %d!\n"), player_num);
|
|
return;
|
|
}
|
|
|
|
if (new_team == players[player_num].team)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("That player is already on that team!\n"));
|
|
return;
|
|
}
|
|
|
|
WRITEUINT8(p, new_team);
|
|
WRITEUINT8(p, player_num);
|
|
|
|
SendNetXCmd(XD_TEAMCHANGE, &buf, p - buf);
|
|
}
|
|
|
|
void P_SetPlayerSpectator(INT32 playernum)
|
|
{
|
|
//Make sure you're in the right gametype.
|
|
if (!G_GametypeHasSpectators())
|
|
return;
|
|
|
|
// Don't duplicate efforts.
|
|
if (players[playernum].spectator)
|
|
return;
|
|
|
|
players[playernum].spectator = true;
|
|
players[playernum].pflags &= ~PF_WANTSTOJOIN;
|
|
G_AssignTeam(&players[playernum], TEAM_UNASSIGNED);
|
|
|
|
players[playernum].playerstate = PST_REBORN;
|
|
}
|
|
|
|
static void Got_Spectate(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 edit_player = READUINT8(*cp);
|
|
UINT8 desired_state = READUINT8(*cp);
|
|
|
|
if (playeringame[edit_player] == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (playernum != playerconsole[edit_player]
|
|
&& playernum != serverplayer
|
|
&& IsPlayerAdmin(playernum) == false)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("Illegal spectate command received from player %s\n"), player_names[playernum]);
|
|
if (server)
|
|
{
|
|
SendKick(playernum, KICK_MSG_CON_FAIL);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (G_GametypeHasSpectators() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
player_t *const player = &players[edit_player];
|
|
|
|
// Safety first!
|
|
const boolean was_spectator = (player->spectator == true);
|
|
if (was_spectator == false)
|
|
{
|
|
if (gamestate == GS_LEVEL && player->mo != NULL)
|
|
{
|
|
// The following will call P_SetPlayerSpectator if successful
|
|
P_DamageMobj(player->mo, NULL, NULL, 1, DMG_SPECTATOR);
|
|
}
|
|
|
|
//...but because the above could return early under some contexts, we try again here
|
|
P_SetPlayerSpectator(edit_player);
|
|
HU_AddChatText(va("\x82*%s became a spectator.", player_names[edit_player]), false);
|
|
}
|
|
|
|
if (desired_state != 0)
|
|
{
|
|
player->pflags |= PF_WANTSTOJOIN;
|
|
}
|
|
else
|
|
{
|
|
player->pflags &= ~PF_WANTSTOJOIN;
|
|
}
|
|
|
|
if (gamestate != GS_LEVEL || was_spectator == true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FinalisePlaystateChange(edit_player);
|
|
}
|
|
|
|
static void Got_TeamChange(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 new_team = READUINT8(*cp);
|
|
UINT8 edit_player = READUINT8(*cp);
|
|
|
|
if (playernum != serverplayer && IsPlayerAdmin(playernum) == false)
|
|
{
|
|
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;
|
|
}
|
|
|
|
if (G_GametypeHasTeams() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (new_team >= TEAM__MAX)
|
|
{
|
|
new_team = TEAM_UNASSIGNED;
|
|
}
|
|
|
|
player_t *const player = &players[edit_player];
|
|
G_AssignTeam(player, new_team);
|
|
|
|
if (player->team == TEAM_UNASSIGNED)
|
|
{
|
|
// auto assign
|
|
G_AutoAssignTeam(player);
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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)
|
|
{
|
|
D_MD5PasswordPass((const UINT8 *)pw, strlen(pw), BASESALT, &adminpassmd5);
|
|
adminpasswordset = true;
|
|
}
|
|
|
|
// Remote Administration
|
|
static void Command_Changepassword_f(void)
|
|
{
|
|
#ifdef NOMD5
|
|
// If we have no MD5 support then completely disable XD_LOGIN responses for security.
|
|
CONS_Alert(CONS_NOTICE, "Remote administration commands are not supported in this build.\n");
|
|
#else
|
|
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>: change remote admin password\n"));
|
|
return;
|
|
}
|
|
|
|
D_SetPassword(COM_Argv(1));
|
|
CONS_Printf(M_GetText("Password set.\n"));
|
|
#endif
|
|
}
|
|
|
|
static void Command_Login_f(void)
|
|
{
|
|
#ifdef NOMD5
|
|
// If we have no MD5 support then completely disable XD_LOGIN responses for security.
|
|
CONS_Alert(CONS_NOTICE, "Remote administration commands are not supported in this build.\n");
|
|
#else
|
|
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;
|
|
}
|
|
|
|
pw = COM_Argv(1);
|
|
|
|
// Do the base pass to get what the server has (or should?)
|
|
D_MD5PasswordPass((const UINT8 *)pw, strlen(pw), BASESALT, &netbuffer->u.md5sum);
|
|
|
|
// Do the final pass to get the comparison the server will come up with
|
|
D_MD5PasswordPass(netbuffer->u.md5sum, 16, va("PNUM%02d", consoleplayer), &netbuffer->u.md5sum);
|
|
|
|
CONS_Printf(M_GetText("Sending login... (Notice only given if password is correct.)\n"));
|
|
|
|
netbuffer->packettype = PT_LOGIN;
|
|
HSendPacket(servernode, true, 0, 16);
|
|
#endif
|
|
}
|
|
|
|
boolean IsPlayerAdmin(INT32 playernum)
|
|
{
|
|
#if 0 // 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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
INT32 playernum = atoi(COM_Argv(1));
|
|
|
|
if (playernum >= 0 && playernum < MAXPLAYERS && playeringame[playernum])
|
|
{
|
|
UINT8 buf[1] = {playernum};
|
|
SendNetXCmd(XD_VERIFIED, buf, 1);
|
|
}
|
|
}
|
|
|
|
static void Got_Verification(const 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)
|
|
{
|
|
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;
|
|
}
|
|
|
|
INT32 playernum = atoi(COM_Argv(1));
|
|
|
|
if (playernum >= 0 && playernum < MAXPLAYERS && playeringame[playernum])
|
|
{
|
|
UINT8 buf[1] = {playernum};
|
|
SendNetXCmd(XD_DEMOTED, buf, 1);
|
|
}
|
|
}
|
|
|
|
static void Got_Removal(const 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)
|
|
{
|
|
extern consvar_t cv_automate;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
void LiveStudioAudience(void)
|
|
{
|
|
if (livestudioaudience_timer == 0)
|
|
{
|
|
S_StartSound(NULL, sfx_mbv91);
|
|
livestudioaudience_timer = 90;
|
|
}
|
|
else
|
|
{
|
|
livestudioaudience_timer--;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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(const 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(const 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 md5 support?
|
|
if (strstr(filename, ".soc") != NULL)
|
|
{
|
|
ncs = findfile(filename, "addons", NULL, 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("Server Connection Failure", va("The server added a file\n(%s)\nthat you do not have.\n",filename), NULL, MM_NOTHING, NULL, "Return to Menu");
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf(M_GetText("Unknown error finding soc file (%s) the server added.\n"), filename);
|
|
M_StartMessage("Server Connection Failure", va("Unknown error trying to load a file\nthat the server added\n(%s).\n",filename), NULL, MM_NOTHING, NULL, "Return to Menu");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
P_RunSOC(filename);
|
|
G_SetGameModified(true, false);
|
|
}
|
|
|
|
/** Adds a pwad at runtime.
|
|
* Searches for sounds, maps, music, new images.
|
|
*/
|
|
static void Command_Addfile(void)
|
|
{
|
|
#ifndef TESTERS
|
|
size_t argc = COM_Argc(); // amount of arguments total
|
|
size_t curarg; // current argument index
|
|
|
|
if (argc < 2)
|
|
{
|
|
CONS_Printf(M_GetText("addfile <filename.pk3/wad/lua/soc> [filename2...] [...]: Load add-ons\n"));
|
|
return;
|
|
}
|
|
|
|
const char **addedfiles = Z_Calloc(sizeof(const char*) * argc, PU_STATIC, NULL);
|
|
size_t numfilesadded = 0; // the amount of filenames processed
|
|
|
|
FILE *fhandle = NULL;
|
|
|
|
// 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 = -1; // W_VerifyNMUSlumps isn't boolean
|
|
|
|
fn = COM_Argv(curarg);
|
|
|
|
// For the amount of filenames previously processed...
|
|
for (ii = 0; ii < numfilesadded; ii++)
|
|
{
|
|
if (strcmp(fn, addedfiles[ii]))
|
|
continue;
|
|
|
|
// If this is one of them, don't try to add it.
|
|
break;
|
|
}
|
|
|
|
// If we've added this one, skip to the next one.
|
|
if (ii < numfilesadded)
|
|
{
|
|
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] != ';')
|
|
continue;
|
|
goto addfile_finally;
|
|
}
|
|
|
|
if (fhandle)
|
|
{
|
|
fclose(fhandle);
|
|
fhandle = NULL;
|
|
}
|
|
|
|
if ((fhandle = W_OpenWadFile(&fn, "addons", true)) != NULL)
|
|
{
|
|
musiconly = W_VerifyNMUSlumps(fn, fhandle, 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 || multiplayer) || musiconly)
|
|
{
|
|
P_AddWadFile(fn);
|
|
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);
|
|
goto addfile_finally;
|
|
}
|
|
|
|
// calculate and check md5
|
|
{
|
|
UINT8 md5sum[16];
|
|
#ifdef NOMD5
|
|
memset(md5sum,0,16);
|
|
#else
|
|
{
|
|
tic_t t = I_GetTime();
|
|
CONS_Debug(DBG_SETUP, "Making MD5 for %s\n",fn);
|
|
md5_stream(fhandle, md5sum);
|
|
CONS_Debug(DBG_SETUP, "MD5 calc for %s took %f second\n", fn, (float)(I_GetTime() - t)/TICRATE);
|
|
}
|
|
|
|
for (i = 0; i < numwadfiles; i++)
|
|
{
|
|
if (memcmp(wadfiles[i]->md5sum, md5sum, 16))
|
|
continue;
|
|
|
|
CONS_Alert(CONS_ERROR, M_GetText("%s is already loaded\n"), fn);
|
|
break;
|
|
}
|
|
|
|
if (i < numwadfiles)
|
|
{
|
|
// Already loaded, try next
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
// Finally okay to write this important data
|
|
WRITESTRINGN(buf_p,p,240);
|
|
WRITEMEM(buf_p, md5sum, 16);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
addfile_finally:
|
|
|
|
if (fhandle)
|
|
fclose(fhandle);
|
|
Z_Free(addedfiles);
|
|
#endif/*TESTERS*/
|
|
}
|
|
|
|
static void Got_RequestAddfilecmd(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
char filename[241];
|
|
filestatus_t ncs = FS_NOTCHECKED;
|
|
UINT8 md5sum[16];
|
|
boolean kick = false;
|
|
boolean toomany = false;
|
|
INT32 i,j;
|
|
|
|
READSTRINGN(*cp, filename, 240);
|
|
READMEM(*cp, md5sum, 16);
|
|
|
|
// 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, "addons", md5sum, 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_MD5SUMBAD)
|
|
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\n", filename));
|
|
}
|
|
|
|
static void Got_Addfilecmd(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
char filename[241];
|
|
filestatus_t ncs = FS_NOTCHECKED;
|
|
UINT8 md5sum[16];
|
|
|
|
READSTRINGN(*cp, filename, 240);
|
|
READMEM(*cp, md5sum, 16);
|
|
|
|
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, "addons", md5sum, true);
|
|
|
|
if (ncs != FS_FOUND || !P_AddWadFile(filename))
|
|
{
|
|
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("Server Connection Failure", va("The server added a file \n(%s)\nbut you have too many files added.\nRestart the game to clear loaded files.\n",filename), NULL, MM_NOTHING, NULL, "Return to Menu");
|
|
}
|
|
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("Server Connection Failure", va("The server added a file \n(%s)\nthat you do not have.\n",filename), NULL, MM_NOTHING, NULL, "Return to Menu");
|
|
}
|
|
else if (ncs == FS_MD5SUMBAD)
|
|
{
|
|
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("Server Connection Failure", va("Checksum mismatch while loading \n%s.\nThe server seems to have a\ndifferent version of this file.\n",filename), NULL, MM_NOTHING, NULL, "Return to Menu");
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf(M_GetText("Unknown error finding wad file (%s) the server added.\n"), filename);
|
|
M_StartMessage("Server Connection Failure", va("Unknown error trying to load a file\nthat the server added \n(%s).\n",filename), NULL, MM_NOTHING, NULL, "Return to Menu");
|
|
}
|
|
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 < mainwads)
|
|
CONS_Printf("\x82 * %.2d\x80: %s\n", i, tempname);
|
|
else if (!wadfiles[i]->important)
|
|
CONS_Printf("\x86 %c %.2d: %s\n", ((i < mainwads + musicwads) ? '*' : ' '), i, tempname);
|
|
else
|
|
CONS_Printf(" %.2d: %s\n", i, tempname);
|
|
}
|
|
}
|
|
|
|
#define MAXDOOMEDNUM 4095
|
|
|
|
static void Command_ListDoomednums_f(void)
|
|
{
|
|
INT16 i, j, focusstart = 0, focusend = 0;
|
|
INT32 argc = COM_Argc(), argstart = 0;
|
|
INT16 table[MAXDOOMEDNUM];
|
|
boolean nodoubles = false;
|
|
UINT8 doubles[(MAXDOOMEDNUM+8/8)];
|
|
|
|
if (argc > 1)
|
|
{
|
|
nodoubles = (strcmp(COM_Argv(1), "-nodoubles") == 0);
|
|
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 %s <-- MAJOR ERROR\n", MOBJTYPE_LIST[i]);
|
|
continue;
|
|
}
|
|
CONS_Printf("\x81""freeslot MT_""%s\n", FREE_MOBJS[i-MT_FIRSTFREESLOT]);
|
|
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);
|
|
if (table[i] < MT_FIRSTFREESLOT)
|
|
{
|
|
CONS_Printf("\x87""hardcode %s\n", MOBJTYPE_LIST[table[i]]);
|
|
continue;
|
|
}
|
|
CONS_Printf("\x81""freeslot MT_""%s\n", FREE_MOBJS[table[i]-MT_FIRSTFREESLOT]);
|
|
}
|
|
}
|
|
|
|
#undef MAXDOOMEDNUM
|
|
|
|
static void Command_cxdiag_f(void)
|
|
{
|
|
UINT16 i, j, errors = 0;
|
|
|
|
CONS_Printf("\x82""Welcome to the Challenge eXception Diagnostic.\n");
|
|
|
|
{
|
|
conditionset_t *c;
|
|
condition_t *cn;
|
|
|
|
CONS_Printf("\x82""Evaluating ConditionSets...\n");
|
|
|
|
for (i = 0; i < MAXCONDITIONSETS; i++)
|
|
{
|
|
c = &conditionSets[i];
|
|
if (!c->numconditions)
|
|
continue;
|
|
|
|
UINT32 lastID = 0;
|
|
boolean validSoFar = true;
|
|
boolean requiresPlaying = false;
|
|
boolean lastRequiresPlaying = false;
|
|
boolean lastrequiredplayingvalid = false;
|
|
boolean immediatelyprefix = false;
|
|
INT32 relevantlevelgt = -1;
|
|
UINT8 lastj = j = 0;
|
|
while (true) //for (j = 0; j < c->numconditions; ++j)
|
|
{
|
|
if (j >= c->numconditions)
|
|
{
|
|
if (j == lastj)
|
|
break;
|
|
UINT8 swap = j;
|
|
j = lastj;
|
|
lastj = swap;
|
|
|
|
lastrequiredplayingvalid = false;
|
|
validSoFar = true;
|
|
}
|
|
|
|
cn = &c->condition[j];
|
|
|
|
if (lastID)
|
|
{
|
|
//if (lastID != cn->id && validSoFar)
|
|
//CONS_Printf("\x87""Condition%d good\n", lastID);
|
|
if (lastID != cn->id)
|
|
{
|
|
lastrequiredplayingvalid = false;
|
|
validSoFar = true;
|
|
if (j != lastj)
|
|
{
|
|
UINT8 swap = j;
|
|
j = lastj;
|
|
lastj = swap;
|
|
continue;
|
|
}
|
|
relevantlevelgt = -1;
|
|
}
|
|
else if (!validSoFar)
|
|
{
|
|
j++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const boolean firstpass = (lastj <= j);
|
|
|
|
if (cn->type == UC_DESCRIPTIONOVERRIDE)
|
|
{
|
|
if (firstpass)
|
|
;
|
|
else if (!cn->stringvar)
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) - Description override has no description!?\n", i+1, j+1, cn->id);
|
|
errors++;
|
|
}
|
|
else if (cn->stringvar[0] != tolower(cn->stringvar[0]))
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) - Description override begins with capital letter, which isn't necessary and can sometimes look weird in generated descriptions\n", i+1, j+1, cn->id);
|
|
errors++;
|
|
}
|
|
lastID = cn->id;
|
|
j++;
|
|
continue;
|
|
}
|
|
|
|
if (cn->type == UC_AND || cn->type == UC_THEN || cn->type == UC_COMMA)
|
|
{
|
|
if (firstpass)
|
|
;
|
|
else if (immediatelyprefix || lastID != cn->id)
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) - Conjunction immediately follows %s - this just looks plain weird!\n", i+1, j+1, cn->id, immediatelyprefix ? "Prefix type" : "start");
|
|
errors++;
|
|
}
|
|
lastID = cn->id;
|
|
j++;
|
|
continue;
|
|
}
|
|
|
|
lastID = cn->id;
|
|
|
|
lastRequiresPlaying = requiresPlaying;
|
|
requiresPlaying = (cn->type >= UCRP_REQUIRESPLAYING);
|
|
|
|
if (!firstpass && lastrequiredplayingvalid)
|
|
{
|
|
if (lastRequiresPlaying != requiresPlaying)
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) combines Playing condition and Statistics condition - will never be achieved\n", i+1, j+1, lastID);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
}
|
|
lastrequiredplayingvalid = true;
|
|
|
|
immediatelyprefix = (cn->type >= UCRP_PREFIX_GRANDPRIX && cn->type <= UCRP_PREFIX_ISMAP);
|
|
|
|
if (cn->type == UCRP_PREFIX_ISMAP || cn->type == UCRP_ISMAP)
|
|
{
|
|
if (firstpass && relevantlevelgt != -1)
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) has multiple courses specified\n", i+1, j+1, lastID);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
if (cn->requirement == 0)
|
|
relevantlevelgt = 0;
|
|
else if (cn->requirement > 0 && cn->requirement < basenummapheaders)
|
|
relevantlevelgt = G_GuessGametypeByTOL(mapheaderinfo[cn->requirement]->typeoflevel);
|
|
else
|
|
relevantlevelgt = -1;
|
|
}
|
|
else if (firstpass || relevantlevelgt == -1)
|
|
;
|
|
else if (cn->type >= UCRP_PODIUMCUP && cn->type <= UCRP_PODIUMNOCONTINUES)
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) is Podium state when specific course in Cup already requested\n", i+1, j+1, lastID);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
else if (cn->type == UCRP_FINISHALLPRISONS || cn->type == UCRP_PREFIX_PRISONBREAK)
|
|
{
|
|
if (!(gametypes[relevantlevelgt]->rules & GTR_PRISONS))
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) is Prison Break-based, but with %s course\n", i+1, j+1, lastID, gametypes[relevantlevelgt]->name);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
}
|
|
else if (cn->type == UCRP_SMASHUFO || cn->type == UCRP_PREFIX_SEALEDSTAR)
|
|
{
|
|
if (!(gametypes[relevantlevelgt]->rules & GTR_CATCHER))
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) is Sealed Star-based, but with %s course\n", i+1, j+1, lastID, gametypes[relevantlevelgt]->name);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
}
|
|
else if (cn->type == UCRP_RINGS || cn->type == UCRP_RINGSEXACT || cn->type == UCRP_RINGDEBT)
|
|
{
|
|
if ((gametypes[relevantlevelgt]->rules & GTR_SPHERES))
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) is Rings-based, but with %s course\n", i+1, j+1, lastID, gametypes[relevantlevelgt]->name);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
}
|
|
else if (cn->type == UCRP_GROWCONSECUTIVEBEAMS || cn->type == UCRP_FAULTED || cn->type == UCRP_FINISHPERFECT)
|
|
{
|
|
if (!(gametypes[relevantlevelgt]->rules & GTR_CIRCUIT))
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) is circuit-based, but with %s course\n", i+1, j+1, lastID, gametypes[relevantlevelgt]->name);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
}
|
|
else if (cn->type == UCRP_FINISHTIMELEFT)
|
|
{
|
|
if (!(gametypes[relevantlevelgt]->rules & GTR_TIMELIMIT))
|
|
{
|
|
CONS_Printf("\x87"" ConditionSet %u entry %u (Condition%u) is timelimit-based, but with %s course\n", i+1, j+1, lastID, gametypes[relevantlevelgt]->name);
|
|
validSoFar = false;
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
|
|
j++;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
unlockable_t *un;
|
|
|
|
CONS_Printf("\x82""Evaluating Challenges...\n");
|
|
|
|
for (i = 0; i < MAXUNLOCKABLES; i++)
|
|
{
|
|
un = &unlockables[i];
|
|
j = un->conditionset;
|
|
if (!j)
|
|
continue;
|
|
|
|
if (!conditionSets[j-1].numconditions)
|
|
{
|
|
CONS_Printf("\x87"" Unlockable %u has ConditionSet %u, which has no Conditions successfully set - will never be unlocked?\n", i+1, j);
|
|
errors++;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
CONS_Printf("\x82""Evaluating Time Medals...\n");
|
|
|
|
for (i = 0; i < numemblems; i++)
|
|
{
|
|
if (emblemlocations[i].type != ET_TIME)
|
|
continue;
|
|
|
|
INT32 checkLevel = M_EmblemMapNum(&emblemlocations[i]);
|
|
|
|
if (checkLevel >= nummapheaders || !mapheaderinfo[checkLevel])
|
|
continue;
|
|
|
|
if (emblemlocations[i].tag > 0)
|
|
{
|
|
if (emblemlocations[i].tag > mapheaderinfo[checkLevel]->ghostCount)
|
|
{
|
|
CONS_Printf("\x87"" Time Medal %u (level %s) has tag %d, which is greater than the number of associated Staff Ghosts (%u)\n", i+1, mapheaderinfo[checkLevel]->lumpname, emblemlocations[i].tag, mapheaderinfo[checkLevel]->ghostCount);
|
|
errors++;
|
|
}
|
|
}
|
|
else switch (emblemlocations[i].tag)
|
|
{
|
|
case 0:
|
|
{
|
|
if (emblemlocations[i].var < TICRATE)
|
|
{
|
|
CONS_Printf("\x87"" Time Medal %u (level %s) is set to %d (less than a second??)\n", i+1, mapheaderinfo[checkLevel]->lumpname, emblemlocations[i].var);
|
|
errors++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case AUTOMEDAL_PLATINUM:
|
|
{
|
|
if (mapheaderinfo[checkLevel]->ghostCount == 0)
|
|
{
|
|
CONS_Printf("\x87"" Time Medal %u (level %s) is AUTOMEDAL_PLATINUM, but there are no associated Staff Ghosts\n", i+1, mapheaderinfo[checkLevel]->lumpname);
|
|
errors++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case AUTOMEDAL_GOLD:
|
|
case AUTOMEDAL_SILVER:
|
|
case AUTOMEDAL_BRONZE:
|
|
{
|
|
if (mapheaderinfo[checkLevel]->ghostCount < 2)
|
|
{
|
|
CONS_Printf("\x87"" Time Medal %u (level %s) is an Auto Medal, but there are %u associated Staff Ghosts, which is less than the 2 recommended minimum\n", i+1, mapheaderinfo[checkLevel]->lumpname, mapheaderinfo[checkLevel]->ghostCount);
|
|
errors++;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
CONS_Printf("\x87"" Time Medal %u (level %s) has invalid tag (%d)\n", i+1, mapheaderinfo[checkLevel]->lumpname, emblemlocations[i].tag);
|
|
errors++;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors)
|
|
CONS_Printf("\x85""%u errors detected.\n", errors);
|
|
else
|
|
CONS_Printf("\x83""No errors detected! Good job\n");
|
|
}
|
|
|
|
void Command_ListUnusedSprites_f(void)
|
|
{
|
|
size_t i, j;
|
|
|
|
CONS_Printf("\x82Printing sprite non-usage...\n");
|
|
|
|
for (i = 0; i < NUMSPRITES; i++)
|
|
{
|
|
if (sprites[i].numframes)
|
|
{
|
|
// We're only showing unused sprites...
|
|
continue;
|
|
}
|
|
|
|
if (i < SPR_FIRSTFREESLOT)
|
|
{
|
|
CONS_Printf(" \x87""hardcode SPR_""%.4s\n", sprnames[i]);
|
|
continue;
|
|
}
|
|
|
|
if (used_spr[(i-SPR_FIRSTFREESLOT)/8] == 0xFF)
|
|
{
|
|
for (j = 0; j < 8; j++)
|
|
{
|
|
CONS_Printf(" \x81""freeslot SPR_""%.4s\n", sprnames[i+j]);
|
|
}
|
|
|
|
i += j;
|
|
}
|
|
|
|
if (used_spr[(i-SPR_FIRSTFREESLOT)/8] & (1<<(i%8)))
|
|
{
|
|
CONS_Printf(" \x81""freeslot SPR_""%.4s\n", sprnames[i]);
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// MISC. COMMANDS
|
|
// =========================================================================
|
|
|
|
/** Prints program version.
|
|
*/
|
|
static void Command_Version_f(void)
|
|
{
|
|
#ifdef DEVELOP
|
|
CONS_Printf("Ring Racers %s %s %s (%s %s)\n", D_GetFancyBranchName(), comprevision, compnote, compdate, comptime);
|
|
#else
|
|
CONS_Printf("Ring Racers %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 ");
|
|
|
|
CONS_Printf("%s ", comptype);
|
|
|
|
// No ASM?
|
|
#ifdef NOASM
|
|
CONS_Printf("\x85" "NOASM " "\x80");
|
|
#endif
|
|
|
|
// DEVELOP build
|
|
#if defined(TESTERS)
|
|
CONS_Printf("\x88" "TESTERS " "\x80");
|
|
#elif 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 App Name: %s\nMod Version: %d\nCode Base:%d\n"), SRB2APPLICATION, 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;
|
|
|
|
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, true))
|
|
{
|
|
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>
|
|
*/
|
|
void PointLimit_OnChange(void);
|
|
void PointLimit_OnChange(void)
|
|
{
|
|
if (K_CanChangeRules(false) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (cv_pointlimit.value == -1)
|
|
{
|
|
CONS_Printf(M_GetText("Point limit will be left up to the gametype.\n"));
|
|
}
|
|
|
|
if (gamestate == GS_LEVEL && leveltime < starttime)
|
|
{
|
|
switch (cv_pointlimit.value)
|
|
{
|
|
case -1:
|
|
break;
|
|
|
|
case 0:
|
|
CONS_Printf(M_GetText("Point limit has been disabled.\n"));
|
|
break;
|
|
|
|
default:
|
|
CONS_Printf(M_GetText("Point limit has been set to %d.\n"), cv_pointlimit.value);
|
|
}
|
|
|
|
g_pointlimit = K_PointLimitForGametype();
|
|
}
|
|
else
|
|
{
|
|
switch (cv_pointlimit.value)
|
|
{
|
|
case -1:
|
|
break;
|
|
|
|
case 0:
|
|
CONS_Printf(M_GetText("Point limit will be disabled next round.\n"));
|
|
break;
|
|
|
|
default:
|
|
CONS_Printf(M_GetText("Point limit will be %d next round.\n"), cv_pointlimit.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void NetTimeout_OnChange(void);
|
|
void NetTimeout_OnChange(void)
|
|
{
|
|
connectiontimeout = (tic_t)cv_nettimeout.value;
|
|
}
|
|
|
|
void JoinTimeout_OnChange(void);
|
|
void JoinTimeout_OnChange(void)
|
|
{
|
|
jointimeout = (tic_t)cv_jointimeout.value;
|
|
}
|
|
|
|
void Lagless_OnChange (void);
|
|
void Lagless_OnChange (void)
|
|
{
|
|
/* don't back out of dishonesty, or go lagless after playing honestly */
|
|
if (cv_lagless.value && gamestate == GS_LEVEL)
|
|
server_lagless = true;
|
|
}
|
|
|
|
UINT32 timelimitintics = 0;
|
|
UINT32 extratimeintics = 0;
|
|
UINT32 secretextratime = 0;
|
|
|
|
UINT32 g_pointlimit = 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
|
|
*/
|
|
void TimeLimit_OnChange(void);
|
|
void TimeLimit_OnChange(void)
|
|
{
|
|
if (K_CanChangeRules(false) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (cv_timelimit.value == -1)
|
|
{
|
|
CONS_Printf(M_GetText("Time limit will be left up to the gametype.\n"));
|
|
}
|
|
|
|
if (gamestate == GS_LEVEL && leveltime < starttime)
|
|
{
|
|
switch (cv_timelimit.value)
|
|
{
|
|
case -1:
|
|
break;
|
|
|
|
case 0:
|
|
CONS_Printf(M_GetText("Time limit has been disabled.\n"));
|
|
break;
|
|
|
|
default:
|
|
CONS_Printf(M_GetText("Time limit has been set to %d second%s.\n"), cv_timelimit.value,cv_timelimit.value == 1 ? "" : "s");
|
|
}
|
|
|
|
timelimitintics = K_TimeLimitForGametype();
|
|
extratimeintics = secretextratime = 0;
|
|
|
|
#ifdef HAVE_DISCORDRPC
|
|
DRPC_UpdatePresence();
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
switch (cv_timelimit.value)
|
|
{
|
|
case -1:
|
|
break;
|
|
|
|
case 0:
|
|
CONS_Printf(M_GetText("Time limit will be disabled next round.\n"));
|
|
break;
|
|
|
|
default:
|
|
CONS_Printf(M_GetText("Time limit will be %d second%s next round.\n"), cv_timelimit.value,cv_timelimit.value == 1 ? "" : "s");
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 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);
|
|
}
|
|
}
|
|
|
|
void Gravity_OnChange(void);
|
|
void Gravity_OnChange(void)
|
|
{
|
|
if (netgame)
|
|
{
|
|
// TODO: multiplayer support
|
|
return;
|
|
}
|
|
|
|
gravity = cv_gravity.value;
|
|
}
|
|
|
|
void SoundTest_OnChange(void);
|
|
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 Command_Showmap_f(void)
|
|
{
|
|
UINT16 printmap = NEXTMAP_INVALID;
|
|
|
|
size_t first_option;
|
|
size_t option_random;
|
|
size_t option_gametype;
|
|
|
|
INT32 newgametype = gametype;
|
|
|
|
char * mapname = NULL;
|
|
char *realmapname = NULL;
|
|
|
|
option_gametype = COM_CheckPartialParm("-g");
|
|
option_random = COM_CheckPartialParm("-r");
|
|
|
|
if (!( first_option = COM_FirstOption() ))
|
|
first_option = COM_Argc();
|
|
|
|
if (option_gametype)
|
|
{
|
|
newgametype = GetGametypeParm(option_gametype);
|
|
if (newgametype == -1)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (option_random)
|
|
{
|
|
UINT8 numPlayers = 0;
|
|
UINT16 oldmapnum = UINT16_MAX;
|
|
if (Playing())
|
|
{
|
|
UINT8 i;
|
|
for (i = 0; i < MAXPLAYERS; ++i)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
extern consvar_t cv_forcebots; // debug
|
|
|
|
if (!(gametypes[newgametype]->rules & GTR_BOTS) && players[i].bot && !cv_forcebots.value)
|
|
{
|
|
// Gametype doesn't support bots
|
|
continue;
|
|
}
|
|
|
|
numPlayers++;
|
|
}
|
|
|
|
oldmapnum = (gamestate == GS_LEVEL)
|
|
? (gamemap-1)
|
|
: prevmap;
|
|
}
|
|
else if (!option_gametype)
|
|
{
|
|
CONS_Printf("Can't use -random from the menu without -gametype.\n");
|
|
return;
|
|
}
|
|
|
|
printmap = G_RandMapPerPlayerCount(G_TOLFlag(newgametype), oldmapnum, false, false, NULL, numPlayers);
|
|
}
|
|
else if (first_option < 2)
|
|
{
|
|
if (!Playing())
|
|
{
|
|
CONS_Printf(M_GetText("You must be in a game to use this.\n"));
|
|
return;
|
|
}
|
|
|
|
printmap = (gamestate == GS_LEVEL)
|
|
? gamemap-1
|
|
: prevmap;
|
|
}
|
|
else
|
|
{
|
|
mapname = ConcatCommandArgv(1, first_option);
|
|
|
|
printmap = G_FindMapByNameOrCode(mapname, &realmapname);
|
|
|
|
if (printmap == 0)
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Could not find any map described as '%s'.\n"), mapname);
|
|
Z_Free(mapname);
|
|
return;
|
|
}
|
|
|
|
printmap--; // i hate the gamemap off-by-one system
|
|
}
|
|
|
|
if (printmap < nummapheaders && mapheaderinfo[printmap])
|
|
{
|
|
char *title = G_BuildMapTitle(printmap + 1);
|
|
|
|
if (mapheaderinfo[printmap]->menuttl[0])
|
|
{
|
|
CONS_Printf("%s (%d): %s / %s\n", mapheaderinfo[printmap]->lumpname, printmap, title, mapheaderinfo[printmap]->menuttl);
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf("%s (%d): %s\n", mapheaderinfo[printmap]->lumpname, printmap, title);
|
|
}
|
|
|
|
Z_Free(title);
|
|
|
|
if ((option_random || first_option < 2) && !option_gametype)
|
|
;
|
|
else if (mapheaderinfo[printmap]->typeoflevel & G_TOLFlag(newgametype))
|
|
{
|
|
CONS_Printf(" compatible with this gametype\n");
|
|
}
|
|
else
|
|
{
|
|
newgametype = G_GuessGametypeByTOL(mapheaderinfo[printmap]->typeoflevel);
|
|
|
|
if (newgametype == -1)
|
|
{
|
|
CONS_Printf(" NOT compatible with any known gametype\n");
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf(" NOT compatible with this gametype (try \"%s\" instead)\n", gametypes[newgametype]->name);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf("Invalid map ID %u\n", printmap);
|
|
}
|
|
|
|
Z_Free(realmapname);
|
|
Z_Free(mapname);
|
|
}
|
|
|
|
static void Command_Mapmd5_f(void)
|
|
{
|
|
if (gamestate == GS_LEVEL)
|
|
{
|
|
INT32 i;
|
|
char md5tmp[33];
|
|
for (i = 0; i < 16; ++i)
|
|
sprintf(&md5tmp[i*2], "%02x", mapmd5[i]);
|
|
CONS_Printf("%s: %s\n", G_BuildMapName(gamemap), md5tmp);
|
|
}
|
|
else
|
|
CONS_Printf(M_GetText("You must be in a level to use this.\n"));
|
|
}
|
|
|
|
boolean G_GamestateUsesExitLevel(void)
|
|
{
|
|
if (demo.playback)
|
|
return false;
|
|
|
|
switch (gamestate)
|
|
{
|
|
case GS_LEVEL:
|
|
case GS_CREDITS:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void Command_ExitLevel_f(void)
|
|
{
|
|
if (!(server || (IsPlayerAdmin(consoleplayer))))
|
|
{
|
|
CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
|
|
}
|
|
else if (K_CanChangeRules(false) == false && CV_CheatsEnabled() == false)
|
|
{
|
|
CONS_Printf(M_GetText("This cannot be used without cheats enabled.\n"));
|
|
}
|
|
else if (G_GamestateUsesExitLevel() == false)
|
|
{
|
|
CONS_Printf(M_GetText("You must be in a level to use this.\n"));
|
|
}
|
|
else
|
|
{
|
|
SendNetXCmd(XD_EXITLEVEL, NULL, 0);
|
|
}
|
|
}
|
|
|
|
static void Got_ExitLevelcmd(const 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;
|
|
}
|
|
|
|
if (G_GamestateUsesExitLevel() == false)
|
|
return;
|
|
|
|
G_FinishExitLevel();
|
|
}
|
|
|
|
static void Got_SetupVotecmd(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
INT16 newGametype = 0;
|
|
boolean baseEncore = false;
|
|
boolean optionalEncore = false;
|
|
INT16 tempVoteLevels[VOTE_NUM_LEVELS][2];
|
|
INT32 i;
|
|
|
|
if (playernum != serverplayer && !IsPlayerAdmin(playernum)) // 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;
|
|
}
|
|
|
|
newGametype = READINT16(*cp);
|
|
baseEncore = (boolean)READUINT8(*cp);
|
|
optionalEncore = (boolean)READUINT8(*cp);
|
|
|
|
if (!(gametyperules & GTR_ENCORE))
|
|
{
|
|
// Strip illegal Encore flags.
|
|
baseEncore = optionalEncore = false;
|
|
}
|
|
|
|
if (newGametype < 0 || newGametype >= numgametypes)
|
|
{
|
|
if (server)
|
|
{
|
|
I_Error("Got_SetupVotecmd: Gametype %d out of range (numgametypes = %d)", newGametype, numgametypes);
|
|
}
|
|
|
|
CONS_Alert(CONS_WARNING, M_GetText("Vote setup with bad gametype %d received from %s\n"), newGametype, player_names[playernum]);
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < VOTE_NUM_LEVELS; i++)
|
|
{
|
|
tempVoteLevels[i][0] = (UINT16)READUINT16(*cp);
|
|
tempVoteLevels[i][1] = (baseEncore == true) ? VOTE_MOD_ENCORE : 0;
|
|
|
|
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;
|
|
}
|
|
|
|
{
|
|
INT16 oldGametype = gametype;
|
|
G_SetGametype(newGametype);
|
|
D_GameTypeChanged(oldGametype);
|
|
}
|
|
|
|
if (optionalEncore == true)
|
|
{
|
|
tempVoteLevels[VOTE_NUM_LEVELS - 1][1] ^= VOTE_MOD_ENCORE;
|
|
}
|
|
|
|
memcpy(g_voteLevels, tempVoteLevels, sizeof(g_voteLevels));
|
|
|
|
// admin can force vote state whenever
|
|
// so we have to save this replay if it needs to be saved
|
|
if (demo.recording)
|
|
G_CheckDemoStatus();
|
|
|
|
G_SetGamestate(GS_VOTING);
|
|
Y_StartVote();
|
|
}
|
|
|
|
static void Got_ModifyVotecmd(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 context = READUINT8(*cp);
|
|
UINT8 targetID = READUINT8(*cp);
|
|
SINT8 vote = READSINT8(*cp);
|
|
|
|
if (context != Y_VoteContext())
|
|
{
|
|
// Silently discard. Server changed the
|
|
// vote type as we were sending our vote.
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
Y_SetPlayersVote(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(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
SINT8 pick = READSINT8(*cp);
|
|
SINT8 level = READSINT8(*cp);
|
|
SINT8 anger = 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, anger);
|
|
}
|
|
|
|
static void Got_ScheduleTaskcmd(const 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(const 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(const 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
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void Got_Cheat(const UINT8 **cp, INT32 playernum)
|
|
{
|
|
UINT8 targetPlayer = READUINT8(*cp);
|
|
cheat_t cheat = READUINT8(*cp);
|
|
|
|
player_t *player;
|
|
|
|
if (cheat >= NUMBER_OF_CHEATS || !CV_CheatsEnabled() || 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))
|
|
{
|
|
UINT32 noclipFlags = MF_NOCLIP;
|
|
|
|
if (player->spectator)
|
|
{
|
|
noclipFlags |= MF_NOCLIPHEIGHT;
|
|
}
|
|
|
|
if (player->mo->flags & MF_NOCLIP)
|
|
{
|
|
player->mo->flags &= ~(noclipFlags);
|
|
status = "off";
|
|
}
|
|
else
|
|
{
|
|
player->mo->flags |= noclipFlags;
|
|
}
|
|
}
|
|
|
|
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->respawn.pointx = x;
|
|
player->respawn.pointy = y;
|
|
player->respawn.pointz = z;
|
|
player->respawn.manual = true;
|
|
|
|
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))
|
|
{
|
|
if (damage >= DMG_INSTAKILL)
|
|
P_KillMobj(player->mo, NULL, NULL, (UINT8)damage);
|
|
else
|
|
P_DamageMobj(player->mo, NULL, NULL, 1, (UINT8)damage);
|
|
}
|
|
|
|
CV_CheaterWarning(targetPlayer, va("damage (flags=%d) 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: {
|
|
SINT8 item = READSINT8(*cp);
|
|
UINT8 amt = READUINT8(*cp);
|
|
|
|
item = max(item, KITEM_SAD);
|
|
item = min(item, NUMKARTITEMS - 1);
|
|
|
|
K_StripItems(player);
|
|
|
|
// Cancel roulette if rolling
|
|
K_StopRoulette(&player->itemRoulette);
|
|
|
|
player->itemtype = item;
|
|
K_SetPlayerItemAmount(player, amt);
|
|
|
|
if (amt == 0)
|
|
{
|
|
CV_CheaterWarning(playernum, "delete my items");
|
|
}
|
|
else
|
|
{
|
|
// FIXME: we should have actual KITEM_ name array
|
|
const char *itemname = cv_kartdebugitem.PossibleValue[1 + item].strvalue;
|
|
|
|
CV_CheaterWarning(playernum, va("give item %s x%d", itemname, amt));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CHEAT_GIVEPOWERUP: {
|
|
UINT8 powerup = READUINT8(*cp);
|
|
UINT16 time = READUINT16(*cp);
|
|
|
|
powerup = min(powerup, LASTPOWERUP);
|
|
|
|
// FIXME: we should have actual KITEM_ name array
|
|
const char *powerupname = cv_kartdebugitem.PossibleValue[
|
|
1 + NUMKARTITEMS + (powerup - FIRSTPOWERUP)].strvalue;
|
|
|
|
K_GivePowerUp(player, powerup, time);
|
|
|
|
CV_CheaterWarning(playernum, va("give powerup %s %d tics", powerupname, time));
|
|
break;
|
|
}
|
|
|
|
case CHEAT_SCORE: {
|
|
UINT32 score = READUINT32(*cp);
|
|
|
|
player->roundscore = score;
|
|
|
|
CV_CheaterWarning(targetPlayer, va("roundscore = %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 CHEAT_RESPAWNAT: {
|
|
INT32 id = READINT32(*cp);
|
|
waypoint_t *finish = K_GetFinishLineWaypoint();
|
|
waypoint_t *waypoint = K_GetWaypointFromID(id);
|
|
path_t path = {0};
|
|
boolean retryBackwards = false;
|
|
const UINT32 baseDist = FixedMul(RESPAWN_DIST, mapobjectscale);
|
|
|
|
CV_CheaterWarning(targetPlayer, va("respawnat %d", id));
|
|
|
|
if (waypoint == NULL)
|
|
{
|
|
CONS_Alert(CONS_WARNING, "respawnat: no waypoint with that ID\n");
|
|
break;
|
|
}
|
|
|
|
// First, just try to go forward normally
|
|
if (K_PathfindToWaypoint(player->respawn.wp, waypoint, &path, false, false))
|
|
{
|
|
// If the path forward is too short, extend it by moving the origin behind
|
|
if (path.totaldist < baseDist)
|
|
{
|
|
retryBackwards = true;
|
|
}
|
|
else
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < path.numnodes; ++i)
|
|
{
|
|
// If we had to cross the finish line, this waypoint is behind us
|
|
if (path.array[i].nodedata == finish)
|
|
{
|
|
retryBackwards = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Z_Free(path.array);
|
|
}
|
|
else
|
|
{
|
|
retryBackwards = true;
|
|
}
|
|
|
|
if (retryBackwards)
|
|
{
|
|
memset(&path, 0, sizeof path);
|
|
if (!K_PathfindThruCircuit(waypoint, baseDist, &path, false, true))
|
|
{
|
|
CONS_Alert(CONS_WARNING, "respawnat: no path to waypoint\n");
|
|
break;
|
|
}
|
|
|
|
// Update origin since lightsnake must go forwards
|
|
player->respawn.wp = path.array[path.numnodes - 1].nodedata;
|
|
|
|
Z_Free(path.array);
|
|
}
|
|
|
|
player->respawn.state = RESPAWNST_NONE;
|
|
K_DoIngameRespawn(player);
|
|
player->respawn.distanceleft = retryBackwards ? baseDist : path.totaldist;
|
|
break;
|
|
}
|
|
|
|
case CHEAT_SPHERES: {
|
|
INT16 spheres = READINT16(*cp);
|
|
|
|
// P_GivePlayerSpheres does value clamping
|
|
player->spheres = 0;
|
|
P_GivePlayerSpheres(player, spheres);
|
|
|
|
CV_CheaterWarning(targetPlayer, va("spheres = %d", spheres));
|
|
break;
|
|
}
|
|
|
|
case CHEAT_AMPS: {
|
|
INT16 amps = READINT16(*cp);
|
|
|
|
player->amps = amps;
|
|
|
|
CV_CheaterWarning(targetPlayer, va("amps = %d", amps));
|
|
break;
|
|
}
|
|
|
|
case CHEAT_FREEZE: {
|
|
const char *status = P_FreezeCheat() ? "off" : "on";
|
|
P_SetFreezeCheat( !P_FreezeCheat() );
|
|
CV_CheaterWarning(targetPlayer, va("freeze %s", status));
|
|
break;
|
|
}
|
|
|
|
case NUMBER_OF_CHEATS:
|
|
break;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
memset(&luabanks, 0, sizeof(luabanks));
|
|
|
|
if (dirmenu)
|
|
closefilemenu(true);
|
|
|
|
if (!modeattacking)
|
|
{
|
|
// YES, this is where demo.attract gets cleared!
|
|
if (demo.attract == DEMO_ATTRACT_CREDITS)
|
|
{
|
|
F_DeferContinueCredits(); // <-- clears demo.attract
|
|
}
|
|
else if (restoreMenu == NULL) // this is true for attract demos too!
|
|
{
|
|
D_StartTitle(); // <-- clears demo.attract
|
|
}
|
|
else
|
|
{
|
|
D_ClearState();
|
|
M_StartControlPanel();
|
|
demo.attract = DEMO_ATTRACT_OFF; // shouldn't ever happen, but let's keep the code symmetrical
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
CONS_Printf(M_GetText("This only works in singleplayer games.\n"));
|
|
}
|
|
else if (grandprixinfo.eventmode == GPEVENT_BONUS)
|
|
{
|
|
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 (savemoddata)
|
|
CONS_Printf("The game has been modified with an addon using its own save data.\n");
|
|
else if (modifiedgame)
|
|
CONS_Printf("The game has been modified, but is still using Ring Racers save data.\n");
|
|
else
|
|
CONS_Printf("The game has not been modified.\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.acp1 != (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_Archive...\n");
|
|
LUA_Archive(&save, true);
|
|
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;
|
|
CONS_Printf("LUA_UnArchive...\n");
|
|
LUA_UnArchive(&save, true);
|
|
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
|
|
|
|
static void Command_DebugMessageFeed(void)
|
|
{
|
|
K_AddMessage("Hello world! A = <a>, Right = <right>", true, false);
|
|
}
|
|
|
|
/** Give yourself an, optional quantity or one of, an item.
|
|
*/
|
|
static void Command_KartGiveItem_f(void)
|
|
{
|
|
UINT8 localplayer = g_localplayers[GetCommandViewNumber()];
|
|
|
|
int ac;
|
|
const char *name;
|
|
INT32 item;
|
|
|
|
const char * str;
|
|
|
|
int i;
|
|
|
|
if (CV_CheatsEnabled())
|
|
{
|
|
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; ( str = kartdebugitem_cons_t[i].strvalue ); ++i)
|
|
{
|
|
if (strcasestr(str, name) != NULL)
|
|
{
|
|
CONS_Printf("\x83\t%s\n", str);
|
|
item = kartdebugitem_cons_t[i].value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item >= FIRSTPOWERUP)
|
|
{
|
|
INT32 amt;
|
|
|
|
if (ac > 2)
|
|
amt = atoi(COM_Argv(2));
|
|
else
|
|
amt = BATTLE_POWERUP_TIME;
|
|
|
|
D_Cheat(localplayer, CHEAT_GIVEPOWERUP, item, amt);
|
|
}
|
|
else 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);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf("This cannot be used without cheats enabled.\n");
|
|
}
|
|
}
|
|
|
|
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_Eval(void)
|
|
{
|
|
const char *args = COM_Args();
|
|
|
|
if (args)
|
|
{
|
|
const fixed_t n = LUA_EvalMath(args);
|
|
|
|
CONS_Printf("%f (%d)\n", FixedToFloat(n), n);
|
|
}
|
|
}
|
|
|
|
static void Command_WriteTextmap(void)
|
|
{
|
|
if (COM_Argc() < 2)
|
|
{
|
|
CONS_Printf(
|
|
"writetextmap <map> [map2...]: Update a map to the latest UDMF version.\n"
|
|
"- Use the full map name, e.g. RR_TestRun.\n"
|
|
"- You can give this command UP TO %d map names and it will convert all of them.\n"
|
|
"- This command generates TEXTMAP files.\n"
|
|
"- The location of the generated TEXTMAPs will appear in the console.\n",
|
|
ROUNDQUEUE_MAX
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (Playing())
|
|
{
|
|
CONS_Alert(CONS_ERROR, "This command cannot be used in-game. Return to the titlescreen first!\n");
|
|
return;
|
|
}
|
|
|
|
if (COM_Argc() - 1 > ROUNDQUEUE_MAX)
|
|
{
|
|
CONS_Alert(CONS_ERROR, "Cannot convert more than %d maps. Try again.\n", ROUNDQUEUE_MAX);
|
|
return;
|
|
}
|
|
|
|
// Start up a "minor" grand prix session
|
|
memset(&grandprixinfo, 0, sizeof(struct grandprixinfo));
|
|
memset(&roundqueue, 0, sizeof(struct roundqueue));
|
|
|
|
grandprixinfo.gamespeed = KARTSPEED_NORMAL;
|
|
grandprixinfo.masterbots = false;
|
|
|
|
grandprixinfo.gp = true;
|
|
grandprixinfo.cup = NULL;
|
|
grandprixinfo.wonround = false;
|
|
|
|
grandprixinfo.initalize = true;
|
|
|
|
roundqueue.position = 1;
|
|
roundqueue.roundnum = 1;
|
|
roundqueue.writetextmap = true;
|
|
|
|
size_t i;
|
|
|
|
for (i = 1; i < COM_Argc(); ++i)
|
|
{
|
|
INT32 map = G_MapNumber(COM_Argv(i));
|
|
|
|
if (map < 0 || map >= nummapheaders)
|
|
{
|
|
CONS_Alert(CONS_WARNING, "%s: Map doesn't exist. Not doing anything.\n", COM_Argv(i));
|
|
|
|
// clear round queue (to be safe)
|
|
memset(&roundqueue, 0, sizeof(struct roundqueue));
|
|
return;
|
|
}
|
|
|
|
INT32 gt = G_GuessGametypeByTOL(mapheaderinfo[map]->typeoflevel);
|
|
|
|
G_MapIntoRoundQueue(map, gt != -1 ? gt : GT_RACE, false, false);
|
|
}
|
|
|
|
D_MapChange(1 + roundqueue.entries[0].mapnum, roundqueue.entries[0].gametype, false, true, 1, false, false);
|
|
|
|
CON_ToggleOff();
|
|
}
|
|
|
|
static void Command_SnapshotMaps(void)
|
|
{
|
|
if (COM_Argc() < 2)
|
|
{
|
|
CONS_Printf(
|
|
"snapshotmap <map> [map2...]: Create a thumbnail screenshot for the specified levels.\n"
|
|
"- You can do partial names, but no spaces (without \"quotes around them\").\n"
|
|
"- You can give this command UP TO %d map names and it will create images for all of them.\n"
|
|
"- This command generates two images -- one 320x200 for the PICTURE lump, another 1024x1024 for rich presence.\n"
|
|
"- The map requires a \"snapshot camera\" object to have been placed.\n"
|
|
"- The location of the generated images will appear in the console.\n",
|
|
ROUNDQUEUE_MAX
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (Playing())
|
|
{
|
|
CONS_Alert(CONS_ERROR, "This command cannot be used in-game. Return to the titlescreen first!\n");
|
|
return;
|
|
}
|
|
|
|
if (COM_Argc() - 1 > ROUNDQUEUE_MAX)
|
|
{
|
|
CONS_Alert(CONS_ERROR, "Cannot snapshot more than %d maps. Try again.\n", ROUNDQUEUE_MAX);
|
|
return;
|
|
}
|
|
|
|
// Start up a "minor" grand prix session
|
|
memset(&grandprixinfo, 0, sizeof(struct grandprixinfo));
|
|
memset(&roundqueue, 0, sizeof(struct roundqueue));
|
|
|
|
grandprixinfo.gamespeed = KARTSPEED_NORMAL;
|
|
grandprixinfo.masterbots = false;
|
|
|
|
grandprixinfo.gp = true;
|
|
grandprixinfo.cup = NULL;
|
|
grandprixinfo.wonround = false;
|
|
|
|
grandprixinfo.initalize = true;
|
|
|
|
roundqueue.position = 1;
|
|
roundqueue.roundnum = 1;
|
|
roundqueue.snapshotmaps = true;
|
|
|
|
size_t i;
|
|
INT32 map;
|
|
const char *mapname;
|
|
char *realmapname;
|
|
|
|
for (i = 1; i < COM_Argc(); ++i)
|
|
{
|
|
mapname = COM_Argv(i);
|
|
map = G_FindMapByNameOrCode(COM_Argv(i), &realmapname); // G_MapNumber(COM_Argv(i));
|
|
|
|
if (map == 0)
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Could not find any map described as '%s'.\nNot snapshotting any maps."), mapname);
|
|
|
|
// clear round queue (to be safe)
|
|
memset(&roundqueue, 0, sizeof(struct roundqueue));
|
|
return;
|
|
}
|
|
|
|
map--;
|
|
|
|
INT32 gt = G_GuessGametypeByTOL(mapheaderinfo[map]->typeoflevel);
|
|
|
|
G_MapIntoRoundQueue(map, gt != -1 ? gt : GT_RACE, false, false);
|
|
|
|
Z_Free(realmapname);
|
|
}
|
|
|
|
D_MapChange(1 + roundqueue.entries[0].mapnum, roundqueue.entries[0].gametype, false, true, 1, false, false);
|
|
|
|
CON_ToggleOff();
|
|
}
|
|
|
|
UINT32 staffsync_map = 0;
|
|
UINT32 staffsync_ghost = 0;
|
|
UINT32 staffsync_done = 0;
|
|
UINT32 staffsync_total = 0;
|
|
UINT32 staffsync_failed = 0;
|
|
staffsync_t staffsync_results[1024];
|
|
|
|
static void Command_Staffsync(void)
|
|
{
|
|
if (Playing())
|
|
{
|
|
CONS_Alert(CONS_ERROR, "This command cannot be used in-game. Return to the titlescreen first!\n");
|
|
return;
|
|
}
|
|
|
|
staffsync = true;
|
|
|
|
if (demo.playback)
|
|
return;
|
|
|
|
mapheader_t *mapheader;
|
|
staffbrief_t *staffbrief;
|
|
|
|
demo.loadfiles = false;
|
|
demo.ignorefiles = true;
|
|
|
|
mapheader = mapheaderinfo[staffsync_map];
|
|
|
|
// Test exit
|
|
if (false && staffsync_done == 9)
|
|
mapheader = NULL;
|
|
|
|
// Start of the run, init progress and report vars
|
|
if (staffsync_map == 0 && staffsync_ghost == 0)
|
|
{
|
|
staffsync_done = 0;
|
|
staffsync_total = 0;
|
|
staffsync_failed = 0;
|
|
memset(staffsync_results, 0, sizeof(staffsync_results));
|
|
for (INT32 i = 0; i < nummapheaders; i++)
|
|
{
|
|
if (mapheaderinfo[i] != NULL)
|
|
staffsync_total += mapheaderinfo[i]->ghostCount;
|
|
}
|
|
|
|
soundtest.shuffle = true;
|
|
S_UpdateSoundTestDef(false, false, true);
|
|
S_SoundTestPlay();
|
|
}
|
|
|
|
// The current configuration corresponds to a valid staff ghost, play it
|
|
if (mapheader != NULL && mapheader->ghostCount > 0 && staffsync_ghost < mapheader->ghostCount)
|
|
{
|
|
demo.timing = true;
|
|
g_singletics = true;
|
|
framecount = 0;
|
|
demostarttime = I_GetTime();
|
|
|
|
staffbrief = mapheader->ghostBrief[staffsync_ghost];
|
|
|
|
G_DoPlayDemoEx("", (staffbrief->wad << 16) | staffbrief->lump);
|
|
|
|
staffsync_ghost++;
|
|
staffsync_done++;
|
|
|
|
if (staffsync_done % 50 == 0)
|
|
{
|
|
S_UpdateSoundTestDef(false, true, true);
|
|
S_SoundTestPlay();
|
|
}
|
|
}
|
|
|
|
// We've exhausted map headers, report
|
|
if (mapheader == NULL)
|
|
{
|
|
|
|
CONS_Printf("========================================\n");
|
|
CONS_Printf("DESYNCING STAFF GHOSTS:\n");
|
|
|
|
UINT32 i = 0;
|
|
while (staffsync_results[i].reason != SYNC_NONE)
|
|
{
|
|
staffsync_t *result = &staffsync_results[i];
|
|
|
|
CONS_Printf("%s [%s] ", G_BuildMapName(result->map), result->name);
|
|
|
|
switch (result->reason)
|
|
{
|
|
case SYNC_CHARACTER:
|
|
CONS_Printf("(character/stats)");
|
|
break;
|
|
case SYNC_HEALTH:
|
|
CONS_Printf("(health)");
|
|
break;
|
|
case SYNC_ITEM:
|
|
CONS_Printf("(item/bumpers)");
|
|
break;
|
|
case SYNC_POSITION:
|
|
CONS_Printf("(position)");
|
|
break;
|
|
case SYNC_RNG:
|
|
CONS_Printf("(RNG class %d)", result->extra);
|
|
break;
|
|
}
|
|
|
|
CONS_Printf("\n");
|
|
|
|
CONS_Printf(" %d syncs (%d error)\n", result->numerror, result->totalerror/FRACUNIT);
|
|
|
|
CONS_Printf(" presync: ");
|
|
|
|
for (UINT32 j = 0; j < PRNUMSYNCED; j++)
|
|
{
|
|
if (result->rngerror_presync[j] > 0)
|
|
CONS_Printf("%s %d ", rng_class_names[j], result->rngerror_presync[j]);
|
|
}
|
|
|
|
CONS_Printf("\n");
|
|
|
|
CONS_Printf(" postsync: ");
|
|
|
|
for (UINT32 j = 0; j < PRNUMSYNCED; j++)
|
|
{
|
|
if (result->rngerror_postsync[j] > 0)
|
|
CONS_Printf("%s %d ", rng_class_names[j], result->rngerror_postsync[j]);
|
|
}
|
|
|
|
CONS_Printf("\n");
|
|
|
|
i++;
|
|
}
|
|
|
|
CONS_Printf("========================================\n");
|
|
|
|
staffsync_ghost = 0;
|
|
staffsync_map = 0;
|
|
staffsync = false;
|
|
|
|
S_SoundTestTogglePause();
|
|
S_StartSound(NULL, sfx_tmxsuc);
|
|
S_StartSound(NULL, sfx_cftbl2);
|
|
|
|
CONS_Printf("%d / %d ghosts desync.\n", staffsync_failed, staffsync_done);
|
|
return;
|
|
}
|
|
|
|
// Out of ghosts for the current map, switch to the next map and re-exec
|
|
if (staffsync_ghost >= mapheader->ghostCount)
|
|
{
|
|
staffsync_ghost = 0;
|
|
staffsync_map++;
|
|
|
|
Command_Staffsync();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
#ifdef DEVELOP
|
|
static void Command_FastForward(void)
|
|
{
|
|
boolean r_flag = false;
|
|
boolean t_flag = false;
|
|
|
|
size_t num_time_args = 0;
|
|
const char *time_arg = NULL;
|
|
|
|
for (size_t i = 1; i < COM_Argc(); ++i)
|
|
{
|
|
const char *arg = COM_Argv(i);
|
|
if (arg[0] == '-')
|
|
{
|
|
while (*++arg)
|
|
{
|
|
switch (*arg)
|
|
{
|
|
case 'r':
|
|
r_flag = true;
|
|
break;
|
|
case 't':
|
|
t_flag = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
time_arg = arg;
|
|
num_time_args++;
|
|
}
|
|
}
|
|
|
|
if (num_time_args != 1)
|
|
{
|
|
CONS_Printf(
|
|
"fastforward [-r] <[mm:]ss>: fast-forward the map time in seconds\n"
|
|
"fastforward -t [-r] <tics>: fast-forward the map time in tics\n"
|
|
"* If the map time has already passed, do nothing.\n"
|
|
"* With -r, fast-forward relative to the current time instead of to an exact map time.\n"
|
|
);
|
|
return;
|
|
}
|
|
|
|
char *p;
|
|
tic_t t = strtol(time_arg, &p, 10);
|
|
|
|
if (!t_flag)
|
|
{
|
|
t *= TICRATE;
|
|
|
|
if (*p == ':')
|
|
{
|
|
t *= 60;
|
|
t += strtol(&p[1], &p, 10) * TICRATE;
|
|
}
|
|
}
|
|
|
|
if (*p)
|
|
{
|
|
CONS_Printf("fastforward: time value is malformed '%s'\n", time_arg);
|
|
return;
|
|
}
|
|
|
|
if (!r_flag)
|
|
{
|
|
if (leveltime > t)
|
|
{
|
|
CONS_Printf("fastforward: leveltime has already passed\n");
|
|
return;
|
|
}
|
|
|
|
t -= leveltime;
|
|
}
|
|
|
|
g_fast_forward = t;
|
|
}
|
|
#endif
|
|
|
|
/** Makes a change to ::cv_forceskin take effect immediately.
|
|
*
|
|
* \sa Command_SetForcedSkin_f, cv_forceskin, forcedskin
|
|
* \author Graue <graue@oceanbase.org>
|
|
*/
|
|
void ForceSkin_OnChange(void);
|
|
void ForceSkin_OnChange(void)
|
|
{
|
|
// NOT in SP, silly!
|
|
if (!Playing() || !K_CanChangeRules(true))
|
|
return;
|
|
|
|
if (cv_forceskin.value < 0)
|
|
CONS_Printf("The server has lifted the forced character restrictions.\n");
|
|
else
|
|
{
|
|
CONS_Printf("The server is restricting all players to \"%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(const UINT8 p)
|
|
{
|
|
if (cv_mute.value && !(server || IsPlayerAdmin(g_localplayers[p])))
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("You may not change your name when chat is muted.\n"));
|
|
CV_StealthSet(&cv_playername[p], player_names[g_localplayers[p]]);
|
|
return;
|
|
}
|
|
|
|
SendNameAndColor(p);
|
|
}
|
|
|
|
void Name1_OnChange(void);
|
|
void Name1_OnChange(void)
|
|
{
|
|
Name_OnChange(0);
|
|
}
|
|
|
|
void Name2_OnChange(void);
|
|
void Name2_OnChange(void)
|
|
{
|
|
Name_OnChange(1);
|
|
}
|
|
|
|
void Name3_OnChange(void);
|
|
void Name3_OnChange(void)
|
|
{
|
|
Name_OnChange(2);
|
|
}
|
|
|
|
void Name4_OnChange(void);
|
|
void Name4_OnChange(void)
|
|
{
|
|
Name_OnChange(3);
|
|
}
|
|
|
|
// sends the follower change for players
|
|
static void FollowerAny_OnChange(UINT8 pnum)
|
|
{
|
|
if (!Playing())
|
|
return; // don't send anything there.
|
|
|
|
SendNameAndColor(pnum);
|
|
G_SetPlayerGamepadIndicatorToPlayerColor(pnum);
|
|
}
|
|
|
|
// sends the follower change for players
|
|
void Follower_OnChange(void);
|
|
void Follower_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(0);
|
|
}
|
|
|
|
// About the same as Color_OnChange but for followers.
|
|
void Followercolor_OnChange(void);
|
|
void Followercolor_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(0);
|
|
}
|
|
|
|
// repeat for the 3 other players
|
|
|
|
void Follower2_OnChange(void);
|
|
void Follower2_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(1);
|
|
}
|
|
|
|
void Followercolor2_OnChange(void);
|
|
void Followercolor2_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(1);
|
|
}
|
|
|
|
void Follower3_OnChange(void);
|
|
void Follower3_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(2);
|
|
}
|
|
|
|
void Followercolor3_OnChange(void);
|
|
void Followercolor3_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(2);
|
|
}
|
|
|
|
void Follower4_OnChange(void);
|
|
void Follower4_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(3);
|
|
}
|
|
|
|
void Followercolor4_OnChange(void);
|
|
void Followercolor4_OnChange(void)
|
|
{
|
|
FollowerAny_OnChange(3);
|
|
}
|
|
|
|
/** Sends a skin change for the console player, unless that player is moving. Also forces them to spectate if the change is done during gameplay
|
|
* \sa cv_skin, Skin2_OnChange, Color_OnChange
|
|
* \author Graue <graue@oceanbase.org>
|
|
*/
|
|
static void Skin_OnChange(const UINT8 p)
|
|
{
|
|
if (!Playing() || splitscreen < p)
|
|
{
|
|
// do whatever you want
|
|
return;
|
|
}
|
|
|
|
if (!CV_CheatsEnabled() && !(netgame || K_CanChangeRules(false))
|
|
&& (gamestate != GS_WAITINGPLAYERS)) // allows command line -warp x +skin y
|
|
{
|
|
CV_StealthSet(&cv_skin[p], skins[players[g_localplayers[p]].skin]->name);
|
|
return;
|
|
}
|
|
|
|
if (CanChangeSkin(g_localplayers[p]))
|
|
{
|
|
SendNameAndColor(p);
|
|
}
|
|
else
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("You can't change your skin at the moment.\n"));
|
|
CV_StealthSet(&cv_skin[p], skins[players[g_localplayers[p]].skin]->name);
|
|
}
|
|
}
|
|
|
|
void Skin1_OnChange(void);
|
|
void Skin1_OnChange(void)
|
|
{
|
|
Skin_OnChange(0);
|
|
}
|
|
|
|
/** Sends a skin change for the secondary splitscreen player, unless that
|
|
* player is moving. Forces spectate the player if the change is done during gameplay.
|
|
* \sa cv_skin2, Skin_OnChange, Color2_OnChange
|
|
* \author Graue <graue@oceanbase.org>
|
|
*/
|
|
void Skin2_OnChange(void);
|
|
void Skin2_OnChange(void)
|
|
{
|
|
Skin_OnChange(1);
|
|
}
|
|
|
|
void Skin3_OnChange(void);
|
|
void Skin3_OnChange(void)
|
|
{
|
|
Skin_OnChange(2);
|
|
}
|
|
|
|
void Skin4_OnChange(void);
|
|
void Skin4_OnChange(void)
|
|
{
|
|
Skin_OnChange(3);
|
|
}
|
|
|
|
/** 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(const UINT8 p)
|
|
{
|
|
I_Assert(p < MAXSPLITSCREENPLAYERS);
|
|
|
|
UINT16 color = cv_playercolor[p].value;
|
|
boolean colorisgood = (color == SKINCOLOR_NONE || K_ColorUsable(color, false, true) == true);
|
|
|
|
if (Playing() && p <= splitscreen)
|
|
{
|
|
if (P_PlayerMoving(g_localplayers[p]) == true)
|
|
{
|
|
colorisgood = false;
|
|
}
|
|
else if (colorisgood == true)
|
|
{
|
|
// Color change menu scrolling fix is no longer necessary
|
|
SendNameAndColor(p);
|
|
}
|
|
}
|
|
|
|
if (colorisgood == false)
|
|
{
|
|
CV_StealthSetValue(&cv_playercolor[p], lastgoodcolor[p]);
|
|
return;
|
|
}
|
|
|
|
lastgoodcolor[p] = color;
|
|
G_SetPlayerGamepadIndicatorToPlayerColor(p);
|
|
}
|
|
|
|
void Color1_OnChange(void);
|
|
void Color1_OnChange(void)
|
|
{
|
|
Color_OnChange(0);
|
|
}
|
|
|
|
/** 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>
|
|
*/
|
|
void Color2_OnChange(void);
|
|
void Color2_OnChange(void)
|
|
{
|
|
Color_OnChange(1);
|
|
}
|
|
|
|
void Color3_OnChange(void);
|
|
void Color3_OnChange(void)
|
|
{
|
|
Color_OnChange(2);
|
|
}
|
|
|
|
void Color4_OnChange(void);
|
|
void Color4_OnChange(void)
|
|
{
|
|
Color_OnChange(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>
|
|
*/
|
|
void Mute_OnChange(void);
|
|
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);
|
|
}
|
|
|
|
void AllowServerVC_OnChange(void);
|
|
void AllowServerVC_OnChange(void)
|
|
{
|
|
if (leveltime <= 1)
|
|
return; // avoid having this notification put in our console / log when we boot the server.
|
|
|
|
if (cv_voice_allowservervoice.value)
|
|
HU_AddChatText(M_GetText("\x82*Voice chat is no longer muted."), false);
|
|
else
|
|
HU_AddChatText(M_GetText("\x82*Voice chat has been 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>
|
|
*/
|
|
void DummyConsvar_OnChange(void);
|
|
void DummyConsvar_OnChange(void)
|
|
{
|
|
extern consvar_t cv_dummyconsvar;
|
|
|
|
if (cv_dummyconsvar.value == 1)
|
|
{
|
|
CV_SetValue(&cv_dummyconsvar, 0);
|
|
CV_ClearChangedFlags();
|
|
}
|
|
}
|
|
|
|
static void Command_ShowScores_f(void)
|
|
{
|
|
if (!(netgame || multiplayer))
|
|
{
|
|
CONS_Printf("This only works with multiple players.\n");
|
|
return;
|
|
}
|
|
|
|
if (K_UsingPowerLevels() != PWRLV_DISABLED)
|
|
{
|
|
CONS_Printf("PWR is currently active - no scores to show!\n");
|
|
return;
|
|
}
|
|
|
|
UINT8 i, j, numplayers = 0;
|
|
UINT8 playerlist[MAXPLAYERS];
|
|
UINT8 pos[MAXPLAYERS];
|
|
UINT16 completed = 0;
|
|
|
|
memset(playerlist, 0, sizeof(playerlist));
|
|
memset(pos, 0, sizeof(pos));
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (playeringame[i] && !players[i].spectator)
|
|
{
|
|
numplayers++;
|
|
continue;
|
|
}
|
|
|
|
completed |= (1 << i);
|
|
}
|
|
|
|
if (!numplayers)
|
|
{
|
|
CONS_Printf("No players are currently in-game.\n");
|
|
return;
|
|
}
|
|
|
|
// This is largely based off of Y_CalculateMatchData.
|
|
|
|
for (j = 0; j < numplayers; j++)
|
|
{
|
|
UINT8 workp = UINT8_MAX;
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (completed & (1 << i))
|
|
continue;
|
|
if (workp != UINT8_MAX && players[workp].score >= players[i].score)
|
|
continue;
|
|
workp = i;
|
|
}
|
|
|
|
completed |= (1 << workp);
|
|
|
|
playerlist[j] = workp;
|
|
|
|
if (j && players[workp].score == players[playerlist[j-1]].score)
|
|
{
|
|
pos[j] = pos[j-1];
|
|
}
|
|
else
|
|
{
|
|
pos[j] = j+1;
|
|
}
|
|
}
|
|
|
|
if (roundqueue.size && roundqueue.position)
|
|
{
|
|
CONS_Printf(
|
|
"Rankings %s Round %u:\n",
|
|
(gamestate == GS_LEVEL) ? "before" : "as of",
|
|
roundqueue.roundnum
|
|
);
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf("Total Rankings:\n");
|
|
}
|
|
|
|
for (i = 0; i < numplayers; i++)
|
|
{
|
|
j = playerlist[i];
|
|
CONS_Printf(" %2u - %*s : %9d\n", pos[i], MAXPLAYERNAME-1, player_names[j], players[j].score); // 9 taken from MAXSCORE
|
|
}
|
|
}
|
|
|
|
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
|
|
void NumLaps_OnChange(void);
|
|
void NumLaps_OnChange(void)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
boolean M_AnyItemsEnabled(void);
|
|
void KartItem_OnChange(void);
|
|
void KartItem_OnChange(void)
|
|
{
|
|
if (netgame && !server)
|
|
return;
|
|
|
|
const boolean check = !M_AnyItemsEnabled();
|
|
if (cv_thunderdome.value != check)
|
|
CV_SetValue(&cv_thunderdome, check);
|
|
}
|
|
|
|
void ThunderDome_MenuSound(void);
|
|
void ThunderDome_OnChange(void);
|
|
void ThunderDome_OnChange(void)
|
|
{
|
|
ThunderDome_MenuSound();
|
|
}
|
|
|
|
void KartFrantic_MenuSound(void);
|
|
void KartFrantic_OnChange(void);
|
|
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"));
|
|
}
|
|
|
|
KartFrantic_MenuSound();
|
|
}
|
|
|
|
void KartSpeed_OnChange(void);
|
|
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("Game speed has been changed to \"%s\".\n"), cv_kartspeed.string);
|
|
gamespeed = (UINT8)cv_kartspeed.value;
|
|
}
|
|
else
|
|
{
|
|
CONS_Printf(M_GetText("Game speed will be changed to \"%s\" next round.\n"), cv_kartspeed.string);
|
|
}
|
|
}
|
|
|
|
void KartEncore_OnChange(void);
|
|
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);
|
|
}
|
|
|
|
void KartEliminateLast_OnChange(void);
|
|
void KartEliminateLast_OnChange(void)
|
|
{
|
|
P_CheckRacers();
|
|
}
|
|
|
|
void Schedule_OnChange(void);
|
|
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 LiveStudioAudience_OnChange(void);
|
|
void LiveStudioAudience_OnChange(void)
|
|
{
|
|
livestudioaudience_timer = 90;
|
|
}
|
|
|
|
void Got_DiscordInfo(const 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
|
|
}
|