Improved entering and exiting out of Observer/Spectator mode

1. General Talk

Not sure if you're aware, however in Half-Life you can use the "spectate" command to enter a "Spectator/Observer" mode.

While in Spectator/Observer mode you can look around the map (for secrets for example) but you can also observe the matches and see what the players are doing, however the "spectate" command has one limitation - if you're in Spectator / Observer mode induced by this command you cannot go back to the game as a player.

Valve decided that it would be too much, I presume so in order for you to go back as player, you need to disconnect from the server and reconnect again.

In my eyes this is a bit inconvenient. I do understand why Valve did it like that, most likely to prevent cheating (I presume).

For my modification Flatline Arena, I decided to lift this limitation. In this tutorial I will show you how to re-implement the exiting and entering in Observer mode.

This one is achieved with around 100 new or edited lines of code.

As part of this tutorial we will do:

  • The "spectate" command will be a toggle now, so you can use it to exit and enter Observer / spectator mode.
  • I will add additional command called "end_spectate" that will pull you out of Observer / Spectator mode.
  • I will add a delay for the use of both "spectate" and "end_spectate" commands so they cannot be spammed.
  • I will create a server command that will control the delay before the using of "spectate" and "end_spectate" commands.

2. Requirements

  • List itemHalf-Life SDK setup to compile in your preferred compiler. In my case I will use Visual Studio 2019

  • List itemText editor. I will use NotePad++ and GitHub Desktop

  • List itemYour mod directory setup and ready to run. I would strongly advise against modifying and playing with the modified library in the original Half-Life

3. Additional notes and disclaimers

  • List itemThis tutorial will work on Half-Life SDK, It might or might not work on any other modified SDK like BugFixed/BugFixed-Rebased/Xash etc... If you're using it on any other SDK that's not officially provided by Valve and it does not work, please do not complain here, as I will disregard the comment.

  • You are free to use this tutorial or part of it in your mod. If you do so however please give credit where credit is due. I will appreciate a small not in the credits for this one. I would also love to see and maybe play your mod.

  • I'm not going to be held responsible for any damage this code can do to your SDK or your mod in general.

OK, since we got those straightened up, let's jump into the fun part. I will work mostly in "client.cpp", "game.cpp", "game.h", "observer.cpp" and "player.h" files. All of those are located in the server side of the SDK.

What I will put under "observer.cpp" can also be put at the bottom of "player.cpp", however IMO it's place is with the remaining of the Observer stuff.

Let us start now.

4. Registering the server cvar for the delay

Open "game.cpp" and look for "allow_spectators". You will get two entries, one where we do "cvar_t" and one for CVAR_REGISTER call.

Add the cvar_t for the new one under the one for "allow_spectators" It should look like this:

cvar_t	spectatate_cmd_delay = { "spectatate_cmd_delay", "30", FCVAR_SERVER };		// how long until we can type spectate or end_spectate again

Now go down to the CVAR_REGISTER and under the one for "allow_spectators " add the new one. It should look like this:

CVAR_REGISTER ( &spectatate_cmd_delay );

We're almost ready with the delay cvar, we need to go and add it to game.h so it could be used everywhere game.h is referenced.

Open game.h and add those two right under the line for "displaysoundlist" like this:

// Spectator settings
extern cvar_t 	allow_spectators;
extern cvar_t 	spectatate_cmd_delay;

If you'd like you can compile now, it should not give any errors, but the cvars will do nothing. We just added it. In the next sections we'll make it work properly.

5. Setting up a proper way to exit or end Observer

Now open player.h and look for "StartObserver", right under it we will add references to our magic that will allow us to exit and stop observer.

Add my lines like this:

// Observer stuff
void StopObserver();
void EndObserver();
float m_flNextSpectCmd;

It's clear what the float will do, we'll use it for the delay on the commands. Now... we need to take care of the two functions we added reference to. Let's do that.

Open "observer.cpp" and at the bottom of the file add my new functions, like this:

C++ code:


extern int gmsgTeamInfo;
extern int g_teamplay;
extern void respawn( entvars_t* pev, BOOL fCopyCorpse );

void CBasePlayer::StopObserver()
{
// Turn off spectator
pev->iuser1 = pev->iuser2 = 0;
m_iHideHUD = 0;

GetClassPtr( (CBasePlayer *)pev )->Spawn();
pev->nextthink = -1;

// Update Team Status
MESSAGE_BEGIN( MSG_ALL, gmsgTeamInfo );
WRITE_BYTE( ENTINDEX( edict() ) ); // index number of primary entity
if( g_teamplay )
WRITE_STRING( TeamID() );
else
WRITE_STRING( "Players" );
MESSAGE_END();

m_fWeapon = FALSE; // force weapon send
m_iHideHUD = 0;
}

void CBasePlayer::EndObserver()
{
m_iHideHUD &= ~(HIDEHUD_HEALTH | HIDEHUD_WEAPONS);
m_afPhysicsFlags &= ~PFLAG_OBSERVER;
pev->iuser1 = 0;
pev->iuser2 = 0;

RemoveAllItems(1);

respawn(pev, false);
}


If you look at those two functions, basically they will do the same thing. Still as one of those will be called for when ending observer and the other one will be called after entering observer there are some differences. I presume you can figure out what they do on your own.

With this conclude this section. You can go and compile the libraries and you should not receive any errors. Yet again the functions are not yet called anywhere so... nothing much will happen.

It's time for the cool stuff.

6. Implementing the spectate toggle command and the end_spectate command

This one cannot be easier, open "client.cpp" and look for "spectate". Then replace the full body of the function with the one I'll provide here.

Note: If you already did some changes to your "spectate" command, just compare what I'm doing and edit accordingly.

C++ code:


else if ( FStrEq( pcmd, "spectate" ) ) // clients wants to become a spectator
{
CBasePlayer *pPlayer = GetClassPtr( (CBasePlayer *)pev );
// Block too offten spectator command usage
if (pPlayer->m_flNextSpectCmd < gpGlobals->time)
{
pPlayer->m_flNextSpectCmd = gpGlobals->time + (spectatate_cmd_delay.value < 1.0 ? 1.0 : spectatate_cmd_delay.value);

  	if (!pPlayer->IsObserver())
  	{
  		// always allow proxies to become a spectator
  		if ((pev->flags & FL_PROXY) || allow_spectators.value)
  		{
  			CBasePlayer\* pPlayer = GetClassPtr((CBasePlayer\*)pev);

  			edict_t\* pentSpawnSpot = g_pGameRules->GetPlayerSpawnSpot(pPlayer);
  			pPlayer->StartObserver(pev->origin, VARS(pentSpawnSpot)->angles);

  			// notify other clients of player switching to spectator mode
  			UTIL_ClientPrintAll(HUD_PRINTNOTIFY, UTIL_VarArgs("%s switched to spectator mode\\n",
  				(pev->netname && STRING(pev->netname)\[0\] != 0) ? STRING(pev->netname) : "unconnected"));
  		}
  		else
  		{
  			ClientPrint(pev, HUD_PRINTCONSOLE, "Spectator mode is disabled.\\n");
  		}
  	}
  	else
  	{
  		// get out of Spectator mode
  		pPlayer->StopObserver();
  		// notify other clients of player left spectators

  		UTIL_ClientPrintAll(HUD_PRINTNOTIFY, UTIL_VarArgs("%s has left spectator mode\\n",
  			(pev->netname && (STRING(pev->netname))\[0\] != 0) ? STRING(pev->netname) : "unconnected"));
  	}
  }

}


We're done with the "spectate" toggle now. Let's do the "end_spectate" command. Just paste this code block under the modified "spectate" code block, like this:

C++ code:


else if ( FStrEq(pcmd, "end_spectate" ) )
{
CBasePlayer *pPlayer = GetClassPtr( (CBasePlayer *)pev );

  if (pPlayer->m_flNextSpectCmd &lt; gpGlobals-&gt;time)
  {
  	pPlayer->m_flNextSpectCmd = gpGlobals->time + (spectatate_cmd_delay.value < 1.0 ? 1.0 : spectatate_cmd_delay.value);

  	pPlayer->EndObserver();

  	// notify other clients of player left spectators
  	UTIL_ClientPrintAll( HUD_PRINTNOTIFY, UTIL_VarArgs( "%s has left spectator mode\\n",
  			( pev->netname && ( STRING( pev->netname ) )\[0\] != 0 ) ? STRING( pev->netname ) : "unconnected" ) );
  }

}


Now, go compile the client and server library, paste them in your mod directory, and let's try the functionality.

Note: You will have to enable the spectate first, as by default it's disabled. This is controlled by the default "allow_spectators" cvar. To enable it you need to set it to 1.

After that you can type "spectate" and you will go into spectator mode. Now you need to wait for 30 seconds and you can type either "spectate" as a toggle, or "end_spectate" to exit the Observer mode.

If you look closely to the code I have given, there is one major difference between "spectate" toggle and "end_spectate". The "spectate" toggle is actually dependent on the "allow_spectators" value, while "end_spectate" is not.

The server administrator might disable the option for spectating on the fly, so in that case "end_spectate" will allow quick getaway from Observer mode with no reconnect/quit needed.

Well this will wrap up this tutorial. You should now have fully functional Spectator mode, with a way to enter and a proper way to exit from it.

7. Closing thoughts

  • If you think the delay is too small and might promote some sort of cheating, just increase it.

  • When adding this one, mind your custom game rules and if those are actually using the Observer mode for something... For example I have LMS game rule, and in that one the player is placed in Observer mode when he's killed, so he can wait for the last man to be resolved or for the time for the round to end. Obviously in that case you would like to disallow the use of the "spectate" toggle and "end_spectate" function... Just make a pointer to your game rule and add a return statement if you're in that game rule. This one can be additionally customized for example by playing around with the spectator modes, disabling the view like in CS (fadetoblack?) or many other things, but that one I will leave to you.

  • You can check out the code in my repository on GitHub, where I intend to add additional tutorials:

GitHub Repository

  • Last by not least important if you like my tutorials and you would like to know more of what I'm doing, feel free to check out the mods I'm working on Cold Ice Remastered and Flatline Arena on ModDB.

FlatLine Arena Homepage

FlatLine Arena ModDB

Cold Ice Remastered ModDB

You can also join our Discord server here to stay up to date with the latest on both mods.

Discord

If you don't like something - MOD IT!

Napoleon was here at some undisclosed time, 2024.