From d1059048f2358f13d8e4b58d3f6d39cdc51abc26 Mon Sep 17 00:00:00 2001 From: greenmarine Date: Sun, 25 May 2008 11:18:15 +0000 Subject: [PATCH] git-svn-id: svn://svn.icculus.org/nexuiz/trunk@3661 f962a42d-fe04-0410-a3ab-8c8b0445ebaa --- data/qcsrc/server/cl_client.qc | 49 ++++++++++++ data/qcsrc/server/cl_impulse.qc | 4 + data/qcsrc/server/cl_weaponsystem.qc | 4 + data/qcsrc/server/clientcommands.qc | 115 +++++++++++++++++++++++++++ data/qcsrc/server/defs.qh | 17 ++++ data/qcsrc/server/g_hook.qc | 3 +- data/qcsrc/server/g_world.qc | 80 +++++++++++++++++++ data/qcsrc/server/miscfunctions.qc | 8 +- data/qcsrc/server/teamplay.qc | 20 +++-- 9 files changed, 289 insertions(+), 11 deletions(-) diff --git a/data/qcsrc/server/cl_client.qc b/data/qcsrc/server/cl_client.qc index 8a611ac25..d80f17cf4 100644 --- a/data/qcsrc/server/cl_client.qc +++ b/data/qcsrc/server/cl_client.qc @@ -979,6 +979,7 @@ void ClientConnect (void) } self.jointime = time; + self.allowedTimeouts = cvar("sv_timeout_number"); } /* @@ -1153,6 +1154,47 @@ void respawn(void) PutClientInServer(); } +/** + * When sv_timeout is used this function returs strings like + * "Timeout begins in 2 seconds!\n" or "Timeout ends in 23 seconds!\n". + * Called by centerprint functions + * @param addOneSecond boolean, set to 1 if the welcome-message centerprint asks for the text + */ +string getTimeoutText(float addOneSecond) { + if (!cvar("sv_timeout") || !timeoutStatus) + return ""; + + local string retStr; + if (timeoutStatus == 1) { + if (addOneSecond == 1) { + retStr = strcat("Timeout begins in ", ftos(remainingLeadTime + 1), " seconds!\n"); + } + else { + retStr = strcat("Timeout begins in ", ftos(remainingLeadTime), " seconds!\n"); + } + return retStr; + } + else if (timeoutStatus == 2) { + if (addOneSecond) { + retStr = strcat("Timeout ends in ", ftos(remainingTimeoutTime + 1), " seconds!\n"); + //don't show messages like "Timeout ends in 0 seconds"... + if ((remainingTimeoutTime + 1) > 0) + return retStr; + else + return ""; + } + else { + retStr = strcat("Timeout ends in ", ftos(remainingTimeoutTime), " seconds!\n"); + //don't show messages like "Timeout ends in 0 seconds"... + if (remainingTimeoutTime > 0) + return retStr; + else + return ""; + } + } + else return ""; +} + void player_powerups (void) { if (g_minstagib) @@ -1652,6 +1694,13 @@ void PlayerPreThink (void) if(frametime > 0) // don't do this in cl_movement frames, just in server ticks UpdateSelectedPlayer(); + //don't allow the player to turn around while game is paused! + if(timeoutStatus == 2) { + self.v_angle = self.lastV_angle; + self.angles = self.lastV_angle; + self.fixangle = TRUE; + } + if (self.deadflag != DEAD_NO) { float button_pressed, force_respawn; diff --git a/data/qcsrc/server/cl_impulse.qc b/data/qcsrc/server/cl_impulse.qc index f3c1c6b6d..9d4c1cdcc 100644 --- a/data/qcsrc/server/cl_impulse.qc +++ b/data/qcsrc/server/cl_impulse.qc @@ -63,6 +63,10 @@ void ImpulseCommands (void) if (!imp || gameover) return; self.impulse = 0; + + if (timeoutStatus == 2) //don't allow any impulses while the game is paused + return; + if (imp >= 1 && imp <= 12) { // weapon switching impulses diff --git a/data/qcsrc/server/cl_weaponsystem.qc b/data/qcsrc/server/cl_weaponsystem.qc index 5afa6d5e4..10da05951 100644 --- a/data/qcsrc/server/cl_weaponsystem.qc +++ b/data/qcsrc/server/cl_weaponsystem.qc @@ -437,6 +437,10 @@ float(float secondary, float attacktime) weapon_prepareattack = self.cnt = self.weapon; return FALSE; } + + if (timeoutStatus == 2) //don't allow the player to shoot while game is paused + return FALSE; + // don't fire if previous attack is not finished if (ATTACK_FINISHED(self) > time + frametime * 0.5) return FALSE; diff --git a/data/qcsrc/server/clientcommands.qc b/data/qcsrc/server/clientcommands.qc index 92566aee4..e43b9b76e 100644 --- a/data/qcsrc/server/clientcommands.qc +++ b/data/qcsrc/server/clientcommands.qc @@ -242,6 +242,9 @@ void SV_ParseClientCommand(string s) { if(tourneyInMatchStage && cvar("g_tourney_disable_spec_vote") && self.classname != "player") { sprint(self, "^1Error: Only players can call a vote during the match-stage.\n"); } + else if(timeoutStatus) { //don't allow a vote call during a timeout + sprint(self, "^1Error: You can not call a vote while a timeout is active.\n"); + } else if(votecalled) { sprint(self, "^1There is already a vote called.\n"); } else { @@ -504,6 +507,10 @@ void SV_ParseClientCommand(string s) { } else if(argv(0) == "ready") { if(cvar("sv_ready_restart")) { + if(timeoutStatus) { + return sprint(self, "^1You cannot reset the game while a timeout is active!\n"); + } + if(!restart_countdown || cvar("sv_ready_restart_repeatable")) { self.ready = TRUE; @@ -558,6 +565,21 @@ void SV_ParseClientCommand(string s) { wordwrap_sprint(cmd, 1111); } else if(argv(0) == "suggestmap") { sprint(self, strcat(MapVote_Suggest(argv(1)), "\n")); + } else if(argv(0) == "calltimeout") { + if(cvar("sv_timeout")) { + if(self.classname == "player") { + if(votecalled) + sprint(self, "^7Error: you can not call a timeout while a vote is active!\n"); + else + evaluateTimeoutCall(); + } + else + sprint(self, "^7Error: only players can call a timeout!\n"); + } + } else if(argv(0) == "resumegame") { + if(cvar("sv_timeout")) { + evaluateResumeGame(); + } } else { cmd = argv(0); /* checks not needed any more since DP has separated clientcommands and regular commands @@ -951,6 +973,15 @@ void ReadyCount() restartAnnouncer.think = restartAnnouncer_Think; restartAnnouncer.nextthink = time; restartAnnouncer.cnt = RESTART_COUNTDOWN; + + //after a restart every players number of allowed timeouts gets reset, too + if(cvar("sv_timeout")) + { + FOR_EACH_REALPLAYER(e) + { + e.allowedTimeouts = cvar("sv_timeout_number"); + } + } //play the prepareforbattle sound to everyone sound(world, CHAN_AUTO, "announcer/robotic/prepareforbattle.wav", 1, ATTN_NONE); @@ -1038,3 +1069,87 @@ void restartAnnouncer_Think() { self.cnt -= 1; } } + +/** + * Checks whether the player who calls the timeout is allowed to do so. + * If so, it initializes the timeout countdown. It also checks whether another + * timeout was already running at this time and reacts correspondingly. + * + * affected globals/fields: .allowedTimeouts, remainingTimeoutTime, remainingLeadTime, + * timeoutInitiator, timeoutStatus, timeoutHandler + * + * This function is called when a player issues the calltimeout command. + */ +void evaluateTimeoutCall() { + if (g_tourney && !tourneyInMatchStage && !cvar("g_tourney_warmup_allow_timeout")) + return sprint(self, "^7Error: You can not call a timeout in warmup-stage!\n"); + if (time < restart_countdown ) + return sprint(self, "^7Error: You can not call a timeout while the map is being restarted!\n"); + if (timeoutStatus != 2) { + //if the map uses a timelimit make sure that timeout cannot be called right before the map ends + if (cvar("timelimit")) { + //a timelimit was used + local float myTl; + if (cvar("timelimit")) + myTl = cvar("timelimit"); + else + myTl = timelimit_orig; + + local float lastPossibleTimeout; + lastPossibleTimeout = (myTl*60) - cvar("sv_timeout_leadtime") - 1; + + if (lastPossibleTimeout < time) + return sprint(self, "^7Error: It is too late to call a timeout now!\n"); + } + } + //player may not call a timeout if he has no calls left + if (self.allowedTimeouts < 1) + return sprint(self, "^7Error: You already used all your timeout calls for this map!\n"); + //now all required checks are passed + self.allowedTimeouts -= 1; + bprint(self.netname, " ^7called a timeout (", ftos(self.allowedTimeouts), " timeouts left)!\n"); //write a bprint who started the timeout (and how many he has left) + remainingTimeoutTime = cvar("sv_timeout_length"); + remainingLeadTime = cvar("sv_timeout_leadtime"); + timeoutInitiator = self; + if (timeoutStatus == 0) { //if another timeout was already active, don't change its status (which was 1 or 2) to 1, only change it to 1 if no timeout was active yet + timeoutStatus = 1; + //create the timeout indicator which centerprints the information to all players and takes care of pausing/unpausing + timeoutHandler = spawn(); + timeoutHandler.think = timeoutHandler_Think; + } + timeoutHandler.nextthink = time; //always let the entity think asap + + //inform all connected clients about the timeout call + sound(world, CHAN_AUTO, "announcer/robotic/timeoutcalled.wav", 1, ATTN_NONE); +} + +/** + * Checks whether a player is allowed to resume the game. If he is allowed to do it, + * and the lead time for the timeout is still active, this countdown just will be aborted (the + * game will never be paused). Otherwise the remainingTimeoutTime will be set to the corresponding + * value of the cvar sv_timeout_resumetime. + * + * This function is called when a player issues the resumegame command. + */ +void evaluateResumeGame() { + if (!timeoutStatus) + return sprint(self, "^7Error: There is no active timeout which could be aborted!\n"); + if (self != timeoutInitiator) + return sprint(self, "^7Error: You may not abort the active timeout. Only the player who called it can do that!\n"); + if (timeoutStatus == 1) { + remainingTimeoutTime = timeoutStatus = 0; + timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately + bprint(strcat("^7The timeout was aborted by ", self.netname, " !\n")); + } + else if (timeoutStatus == 2) { + //only shorten the remainingTimeoutTime if it makes sense + if( remainingTimeoutTime > (cvar("sv_timeout_resumetime") + 1) ) { + bprint(strcat("^1Attention: ^7", self.netname, " resumed the game! Prepare for battle!\n")); + remainingTimeoutTime = cvar("sv_timeout_resumetime"); + timeoutHandler.nextthink = time; //timeoutHandler has to take care of it immediately + } + else + sprint(self, "^7Error: Your resumegame call was discarded!\n"); + + } +} diff --git a/data/qcsrc/server/defs.qh b/data/qcsrc/server/defs.qh index 43c9bd90c..e9c2438b2 100644 --- a/data/qcsrc/server/defs.qh +++ b/data/qcsrc/server/defs.qh @@ -292,6 +292,23 @@ float timelimit_orig; float isJoinAllowed(); #define PREVENT_JOIN_TEXT "^1You may not join the game at this time.\n\nThe player limit reached maximum capacity." + +//sv_timeout: pauses the game by setting the gamespeed to a really low value (see TIMEOUT_SLOWMO_VALUE) +#define TIMEOUT_SLOWMO_VALUE 0.0001 +float sys_ticrate; // gets initialised in worlspawn, saves the value from cvar("sys_ticrate") +float remainingTimeoutTime; // contains the time in seconds that the active timeout has left +float remainingLeadTime; // contains the number of seconds left of the leadtime (before the timeout starts) +float timeoutStatus; // (values: 0, 1, 2) contains whether a timeout is not active (0), was called but still at leadtime (1) or is active (2) +.float allowedTimeouts; // contains the number of allowed timeouts for each player +entity timeoutInitiator; // contains the entity of the player who started the last timeout +float orig_slowmo; // contains the value of cvar("slowmo") so that, after timeout finished, it isn't set to slowmo 1 necessarily +.vector lastV_angle; //used when pausing the game in order to force the player to keep his old view angle fixed +entity timeoutHandler; //responsible for centerprinting the timeout countdowns and playing sounds +void timeoutHandler_Think(); +void evaluateTimeoutCall(); +void evaluateResumeGame(); +string getTimeoutText(float addOneSecond); + .float spawnshieldtime; .float lms_nextcheck; diff --git a/data/qcsrc/server/g_hook.qc b/data/qcsrc/server/g_hook.qc index 107dc5a03..0981c018b 100644 --- a/data/qcsrc/server/g_hook.qc +++ b/data/qcsrc/server/g_hook.qc @@ -249,7 +249,8 @@ void GrapplingHookFrame() if (self.button6 && g_grappling_hook) { if (!self.hook && self.hook_time <= time && !self.button6_pressed_before) - FireGrapplingHook(); + if (timeoutStatus != 2) //only allow the player to fire the grappling hook if the game is not paused (timeout) + FireGrapplingHook(); } else { diff --git a/data/qcsrc/server/g_world.qc b/data/qcsrc/server/g_world.qc index d1a50d976..5d4838dd7 100644 --- a/data/qcsrc/server/g_world.qc +++ b/data/qcsrc/server/g_world.qc @@ -55,6 +55,82 @@ void fteqcc_testbugs() world.frags = 0; } +/** + * Takes care of pausing and unpausing the game. + * Centerprints the information about an upcoming or active timeout to all active + * players. Also plays reminder sounds. + */ +void timeoutHandler_Think() { + local string timeStr; + local entity plr; + if (timeoutStatus == 1) { + if (remainingLeadTime > 0) { + //centerprint the information to every player + timeStr = getTimeoutText(0); + FOR_EACH_REALCLIENT(plr) { + if(plr.classname == "player") { + centerprint_atprio(plr, CENTERPRIO_SPAM, timeStr); + } + } + remainingLeadTime -= 1; + //think again in 1 second: + self.nextthink = time + 1; + } + else { + //now pause the game: + timeoutStatus = 2; + cvar_set("slowmo", ftos(TIMEOUT_SLOWMO_VALUE)); + //copy .v_angle to .lastV_angle for every player in order to fix their view during pause (see PlayerPreThink) + FOR_EACH_REALPLAYER(plr) { + plr.lastV_angle = plr.v_angle; + } + self.nextthink = time; + } + } + else if (timeoutStatus == 2) { + if (remainingTimeoutTime > 0) { + timeStr = getTimeoutText(0); + FOR_EACH_REALCLIENT(plr) { + if(plr.classname == "player") { + centerprint_atprio(plr, CENTERPRIO_SPAM, timeStr); + } + } + if(remainingTimeoutTime == cvar("sv_timeout_resumetime")) { //play a warning sound when only seconds are left + sound(world, CHAN_AUTO, "announcer/robotic/prepareforbattle.wav", 1, ATTN_NONE); + } + remainingTimeoutTime -= 1; + self.nextthink = time + TIMEOUT_SLOWMO_VALUE; + } + else { + //unpause the game again + remainingTimeoutTime = timeoutStatus = 0; + cvar_set("slowmo", ftos(orig_slowmo)); + //and unlock the fixed view again once there is no timeout active anymore + FOR_EACH_REALPLAYER(plr) { + plr.fixangle = FALSE; + } + //get rid of the countdown message + FOR_EACH_REALCLIENT(plr) { + if(plr.classname == "player") { + centerprint_atprio(plr, CENTERPRIO_SPAM, ""); + } + } + remove(self); + return; + } + + } + else if (timeoutStatus == 0) { //if a player called the resumegame command (which set timeoutStatus to 0 already) + FOR_EACH_REALCLIENT(plr) { + if(plr.classname == "player") { + centerprint_atprio(plr, CENTERPRIO_SPAM, ""); + } + } + remove(self); + return; + } +} + float GotoFirstMap() { if(cvar("_sv_init")) @@ -240,6 +316,10 @@ void worldspawn (void) Ban_LoadBans(); + //initialise globals related to sv_timeout + sys_ticrate = cvar("sys_ticrate"); + orig_slowmo = cvar("slowmo"); + #ifdef MAPINFO MapInfo_Enumerate(); MapInfo_FilterGametype(MapInfo_CurrentGametype(), MapInfo_CurrentFeatures(), 1); diff --git a/data/qcsrc/server/miscfunctions.qc b/data/qcsrc/server/miscfunctions.qc index 471e04b0a..a714b0a67 100644 --- a/data/qcsrc/server/miscfunctions.qc +++ b/data/qcsrc/server/miscfunctions.qc @@ -638,7 +638,10 @@ void centerprint_atprio(entity e, float prio, string s) if(prio >= e.centerprint_priority) { e.centerprint_priority = prio; - e.centerprint_expires = time + e.cvar_scr_centertime; + if(timeoutStatus == 2) + e.centerprint_expires = time + (e.cvar_scr_centertime * TIMEOUT_SLOWMO_VALUE); + else + e.centerprint_expires = time + e.cvar_scr_centertime; centerprint_builtin(e, s); } } @@ -1038,8 +1041,9 @@ void precache() precache_sound ("announcer/male/yoda.wav"); // announcer sounds - robotic - precache_sound ("announcer/robotic/begin.wav"); precache_sound ("announcer/robotic/prepareforbattle.wav"); + precache_sound ("announcer/robotic/begin.wav"); + precache_sound ("announcer/robotic/timeoutcalled.wav"); precache_sound ("announcer/robotic/1fragleft.wav"); precache_sound ("announcer/robotic/1minuteremains.wav"); precache_sound ("announcer/robotic/2fragsleft.wav"); diff --git a/data/qcsrc/server/teamplay.qc b/data/qcsrc/server/teamplay.qc index d5ba2a7d0..89377b6fc 100644 --- a/data/qcsrc/server/teamplay.qc +++ b/data/qcsrc/server/teamplay.qc @@ -354,7 +354,7 @@ void PrintWelcomeMessage(entity pl) self.welcomemessage_time = time + 0.8; */ if(self.cvar_scr_centertime == 0) return; - if( !(time < restart_countdown) ) { //really print the WelcomeMessage to the player every frame when the game is restarted, to make sure that the shown number is accurate + if( !(timeoutStatus >= 1 || (time < restart_countdown) ) ) { //really print the WelcomeMessage to the player every frame when timeout-seconds are shown or the game is restarted, to make sure that the shown number is accurate if(self.welcomemessage_time > time) return; self.welcomemessage_time = time + self.cvar_scr_centertime * 0.6; } @@ -378,14 +378,15 @@ void PrintWelcomeMessage(entity pl) { if ((g_lms && self.frags < 1) || g_arena) return centerprint_atprio(self, CENTERPRIO_SPAM, strcat(NEWLINES, "spectating ", self.enemy.netname, "\n\n\n^7press attack for next player\npress attack2 for free fly mode")); - else - { - local string spectatorText; - spectatorText = strcat(NEWLINES, "spectating ", self.enemy.netname, "\n\n\n^7press jump to play\n^7press attack for next player\npress attack2 for free fly mode"); - + else { + local string specString; + specString = strcat(NEWLINES, "spectating ", self.enemy.netname, "\n\n\n^7press jump to play\n^7press attack for next player\npress attack2 for free fly mode"); + if(time < restart_countdown) //also show the countdown when being a spectator - spectatorText = strcat(spectatorText, "\n\n^1Game starts in ", ftos(restartAnnouncer.cnt + 1), " seconds^7"); - return centerprint_atprio(self, CENTERPRIO_SPAM, spectatorText); + specString = strcat(specString, "\n\n^1Game starts in ", ftos(restartAnnouncer.cnt + 1), " seconds^7"); + else if (timeoutStatus != 0) + specString = strcat(specString, "\n\n", getTimeoutText(1)); + return centerprint_atprio(self, CENTERPRIO_SPAM, specString); } } } @@ -444,6 +445,9 @@ void PrintWelcomeMessage(entity pl) if(time < restart_countdown) s = strcat(s, "\n^1Game starts in ", ftos(restartAnnouncer.cnt + 1), " seconds^7"); + if(timeoutStatus != 0) + s = strcat(s, "\n\n", getTimeoutText(1)); + s = strzone(s); if (g_grappling_hook) -- 2.39.2