Adding Ironsights

From Valve Developer Community
Jump to navigation Jump to search
English (en)中文 (zh)Translate (Translate)

This is a tutorial how to add procedural ironsighting and is an alternative to Jorg's code.

This approach is networked and allows you to control the toggling server-side, thus making it possible to affect game logic (e.g. add different bullet spread) easily. It also has working angles and fov-change.

Weaponscript

To begin, add script-variables to be able to adjust the ironsight-offset via the weapon-script.

weapon_parse.h

Add this to the member-variables of FileWeaponInfo_t:

	Vector					vecIronsightPosOffset;
	QAngle					angIronsightAngOffset;
	float					flIronsightFOVOffset;

weapon_parse.cpp

This goes to FileWeaponInfo_t::Parse:

	KeyValues *pSights = pKeyValuesData->FindKey( "IronSight" );
	if (pSights)
	{
		vecIronsightPosOffset.x		= pSights->GetFloat( "forward", 0.0f );
		vecIronsightPosOffset.y		= pSights->GetFloat( "right", 0.0f );
		vecIronsightPosOffset.z		= pSights->GetFloat( "up", 0.0f );

		angIronsightAngOffset[PITCH]	= pSights->GetFloat( "pitch", 0.0f );
		angIronsightAngOffset[YAW]		= pSights->GetFloat( "yaw", 0.0f );
		angIronsightAngOffset[ROLL]		= pSights->GetFloat( "roll", 0.0f );

		flIronsightFOVOffset		= pSights->GetFloat( "fov", 0.0f );
	}
	else
	{
		//note: you can set a bool here if you'd like to disable ironsights for weapons with no IronSight-key
		vecIronsightPosOffset = vec3_origin;
		angIronsightAngOffset.Init();
		flIronsightFOVOffset = 0.0f;
	}

weapon_smg1.txt

This is an example of how to add the ironsight-offsets to your script:

	IronSight
	{
		"forward"	"-10"
		"right"		"-6.91"
		"up"		"0.185"
		"roll"		"-20"
		"fov"		"-20"
	}

Getting the offsets

Now add simple functions to get the information we parsed from the weapon-scripts. Additionally add ConVars to overwrite the parsed info so we can easily make and adjust new ironsight-offsets via the console.

Before that however, we need to include "c_baseplayer.h" as a lot of this code references to this file for client side processing.

basecombatweapon_shared.cpp

Basicaly, you want to include "c_baseplayer.h" within #ifdef CLIENT_DLL tags, otherwise it will throw errors when trying to compile.

#ifdef CLIENT_DLL  
 	#include "c_baseplayer.h"
 #endif

You would want to add that right below the:

#if defined ( TF_DLL ) || defined ( TF_CLIENT_DLL )
  #include "tf_shareddefs.h"
#endif

basecombatweapon_shared.h

This goes to the public functions:

	Vector					GetIronsightPositionOffset( void ) const;
	QAngle					GetIronsightAngleOffset( void ) const;
	float					GetIronsightFOVOffset( void ) const;

basecombatweapon_shared.cpp

And then you add the function definitions: (were does this go is causes errors)

Vector CBaseCombatWeapon::GetIronsightPositionOffset( void ) const
{
	if( viewmodel_adjust_enabled.GetBool() )
		return Vector( viewmodel_adjust_forward.GetFloat(), viewmodel_adjust_right.GetFloat(), viewmodel_adjust_up.GetFloat() );
	return GetWpnData().vecIronsightPosOffset;
}

QAngle CBaseCombatWeapon::GetIronsightAngleOffset( void ) const
{
	if( viewmodel_adjust_enabled.GetBool() )
		return QAngle( viewmodel_adjust_pitch.GetFloat(), viewmodel_adjust_yaw.GetFloat(), viewmodel_adjust_roll.GetFloat() );
	return GetWpnData().angIronsightAngOffset;
}

float CBaseCombatWeapon::GetIronsightFOVOffset( void ) const
{
	if( viewmodel_adjust_enabled.GetBool() )
		return viewmodel_adjust_fov.GetFloat();
	return GetWpnData().flIronsightFOVOffset;
}

basecombatweapon_shared.cpp

These ConVars usually go after the includes:

//forward declarations of callbacks used by viewmodel_adjust_enable and viewmodel_adjust_fov
void vm_adjust_enable_callback( IConVar *pConVar, char const *pOldString, float flOldValue );
void vm_adjust_fov_callback( IConVar *pConVar, const char *pOldString, float flOldValue );

ConVar viewmodel_adjust_forward( "viewmodel_adjust_forward", "0", FCVAR_REPLICATED );
ConVar viewmodel_adjust_right( "viewmodel_adjust_right", "0", FCVAR_REPLICATED );
ConVar viewmodel_adjust_up( "viewmodel_adjust_up", "0", FCVAR_REPLICATED );
ConVar viewmodel_adjust_pitch( "viewmodel_adjust_pitch", "0", FCVAR_REPLICATED );
ConVar viewmodel_adjust_yaw( "viewmodel_adjust_yaw", "0", FCVAR_REPLICATED );
ConVar viewmodel_adjust_roll( "viewmodel_adjust_roll", "0", FCVAR_REPLICATED );
ConVar viewmodel_adjust_fov( "viewmodel_adjust_fov", "0", FCVAR_REPLICATED, "Note: this feature is not available during any kind of zoom", vm_adjust_fov_callback );
ConVar viewmodel_adjust_enabled( "viewmodel_adjust_enabled", "0", FCVAR_REPLICATED|FCVAR_CHEAT, "enabled viewmodel adjusting", vm_adjust_enable_callback );

And implement the callbacks.

void vm_adjust_enable_callback( IConVar *pConVar, char const *pOldString, float flOldValue )
{
	ConVarRef sv_cheats( "sv_cheats" );
	if( !sv_cheats.IsValid() || sv_cheats.GetBool() )
		return;

	ConVarRef var( pConVar );

	if( var.GetBool() )
		var.SetValue( "0" );
}

void vm_adjust_fov_callback( IConVar *pConVar, char const *pOldString, float flOldValue )
{
	if( !viewmodel_adjust_enabled.GetBool() )
		return;

	ConVarRef var( pConVar );

	CBasePlayer *pPlayer = 
#ifdef GAME_DLL
		UTIL_GetCommandClient();
#else
		C_BasePlayer::GetLocalPlayer();
#endif
	if( !pPlayer )
		return;

	if( !pPlayer->SetFOV( pPlayer, pPlayer->GetDefaultFOV() + var.GetFloat(), 0.1f ) )
	{
		Warning( "Could not set FOV\n" );
		var.SetValue( "0" );
	}
}

Adding toggle-functions

Of course you also want to be able to use the sights.

basecombatweapon_shared.h

Add these two networked variables:

	CNetworkVar( bool, m_bIsIronsighted );
	CNetworkVar( float, m_flIronsightedTime );

basecombatweapon_shared.cpp

Network them, let them be predicted and give them default values.

Constructor:

	m_bIsIronsighted = false;
	m_flIronsightedTime = 0.0f;


Network-table (DT_BaseCombatWeapon):

	SendPropBool( SENDINFO( m_bIsIronsighted ) ),
	SendPropFloat( SENDINFO( m_flIronsightedTime ) ),

and

	RecvPropInt( RECVINFO( m_bIsIronsighted ), 0, RecvProxy_ToggleSights ), //note: RecvPropBool is actually RecvPropInt (see its implementation), but we need a proxy
	RecvPropFloat( RECVINFO( m_flIronsightedTime ) ),

As mentioned, we need a RecvProxy on that variable. We don't just want the boolean to update, we also want the ironsights to toggle if it was changed only on the server.

So, here's the code for the proxy:

#ifdef CLIENT_DLL
void RecvProxy_ToggleSights( const CRecvProxyData* pData, void* pStruct, void* pOut )
{
	CBaseCombatWeapon *pWeapon = (CBaseCombatWeapon*)pStruct;
	if( pData->m_Value.m_Int )
		pWeapon->EnableIronsights();
	else
		pWeapon->DisableIronsights();
}
#endif

Prediction-table (CBaseCombatWeapon):

	DEFINE_PRED_FIELD( m_bIsIronsighted, FIELD_BOOLEAN, FTYPEDESC_INSENDTABLE ),
	DEFINE_PRED_FIELD( m_flIronsightedTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE ),


If you want the variables to be saved in a save-game, also add them in the data-description:

	DEFINE_FIELD( m_bIsIronsighted, FIELD_BOOLEAN ),
	DEFINE_FIELD( m_flIronsightedTime, FIELD_FLOAT ),

basecombatweapon_shared.h

Now add accessors for the ironsight-variables:

	virtual bool				HasIronsights( void ) { return true; } //default yes; override and return false for weapons with no ironsights (like weapon_crowbar)
	bool					IsIronsighted( void );
	void					ToggleIronsights( void );
	void					EnableIronsights( void );
	void					DisableIronsights( void );
	void					SetIronsightTime( void );

basecombatweapon_shared.cpp

And of course define them:

bool CBaseCombatWeapon::IsIronsighted( void )
{
	return ( m_bIsIronsighted || viewmodel_adjust_enabled.GetBool() );
}

void CBaseCombatWeapon::ToggleIronsights( void )
{
	if( m_bIsIronsighted )
		DisableIronsights();
	else
		EnableIronsights();
}

void CBaseCombatWeapon::EnableIronsights( void )
{
#ifdef CLIENT_DLL
	if( !prediction->IsFirstTimePredicted() )
		return;
#endif
	if( !HasIronsights() || m_bIsIronsighted )
		return;

	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );

	if( !pOwner )
		return;

	if( pOwner->SetFOV( this, pOwner->GetDefaultFOV() + GetIronsightFOVOffset(), 1.0f ) ) //modify the last value to adjust how fast the fov is applied
	{
		m_bIsIronsighted = true;
		SetIronsightTime();
	}
}

void CBaseCombatWeapon::DisableIronsights( void )
{
#ifdef CLIENT_DLL
	if( !prediction->IsFirstTimePredicted() )
		return;
#endif
	if( !HasIronsights() || !m_bIsIronsighted )
		return;

	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );

	if( !pOwner )
		return;

	if( pOwner->SetFOV( this, 0, 0.4f ) ) //modify the last value to adjust how fast the fov is applied
	{
		m_bIsIronsighted = false;
		SetIronsightTime();
	}
}

void CBaseCombatWeapon::SetIronsightTime( void )
{
	m_flIronsightedTime = gpGlobals->curtime;
}

The usage of prediction requires the inclusion of the prediction.h header on the client.

Toggle-command

ConCommand

You probably want a command to switch between normal and sighted mode. It doesn't really matter where you put it, as long as you include the headers for CBasePlayer and CBaseCombatWeapon and it's client-side.

#ifdef CLIENT_DLL
void CC_ToggleIronSights( void )
{
	CBasePlayer* pPlayer = C_BasePlayer::GetLocalPlayer();
	if( pPlayer == NULL )
		return;

	CBaseCombatWeapon *pWeapon = pPlayer->GetActiveWeapon();
	if( pWeapon == NULL )
		return;

	pWeapon->ToggleIronsights();

	engine->ServerCmd( "toggle_ironsight" ); //forward to server
}

static ConCommand toggle_ironsight("toggle_ironsight", CC_ToggleIronSights);
#endif

player.cpp

Then in CBasePlayer::ClientCommand, add this before returning false:

else if( stricmp( cmd, "toggle_ironsight" ) == 0 )
{
	CBaseCombatWeapon *pWeapon = GetActiveWeapon();
	if( pWeapon != NULL )
		pWeapon->ToggleIronsights();

	return true;
}

Automatic ironsight-toggle

basecombatweapon_shared.cpp

Add

DisableIronsights();

to the following places:

  • bool CBaseCombatWeapon::Holster( CBaseCombatWeapon *pSwitchingTo ) for switching/holstering weapons
  • bool CBaseCombatWeapon::DefaultReload( int iClipSize1, int iClipSize2, int iActivity ) for reloading weapons
  • void CBaseCombatWeapon::Drop( const Vector &vecVelocity ) for weapon dropping


Note.pngNote:bool CBaseCombatWeapon::DefaultDeploy - Much better also write function of Disable Iron Sight in Deploy function. This will help with auto-switching when all the ammo has been used.


weapon_shotgun.cpp

Find: bool CWeaponShotgun::StartReload( void )

and some were inside Add

DisableIronsights();

Adjust the viewmodel

Ok, now for the last step we want to move the viewmodel according to the offsets.

There are still some little issues to fix, though. Viewmodel-bob is still active while iron-sighting and the viewmodel lags way too much when going out of ironsight and rotating the view.

baseviewmodel_shared.cpp

void CBaseViewModel::CalcIronsights( Vector &pos, QAngle &ang )
{
	CBaseCombatWeapon *pWeapon = GetOwningWeapon();

	if ( !pWeapon )
		return;

	//get delta time for interpolation
	float delta = ( gpGlobals->curtime - pWeapon->m_flIronsightedTime ) * 2.5f; //modify this value to adjust how fast the interpolation is
	float exp = ( pWeapon->IsIronsighted() ) ? 
		( delta > 1.0f ) ? 1.0f : delta : //normal blending
		( delta > 1.0f ) ? 0.0f : 1.0f - delta; //reverse interpolation

	if( exp <= 0.001f ) //fully not ironsighted; save performance
		return;

	Vector newPos = pos;
	QAngle newAng = ang;

	Vector vForward, vRight, vUp, vOffset;
	AngleVectors( newAng, &vForward, &vRight, &vUp );
	vOffset = pWeapon->GetIronsightPositionOffset();

	newPos += vForward * vOffset.x;
	newPos += vRight * vOffset.y;
	newPos += vUp * vOffset.z;
	newAng += pWeapon->GetIronsightAngleOffset();
	//fov is handled by CBaseCombatWeapon

	pos += ( newPos - pos ) * exp;
	ang += ( newAng - ang ) * exp;
}

Make use of this code in CBaseViewModel::CalcViewModelView:

void CBaseViewModel::CalcViewModelView( CBasePlayer *owner, const Vector& eyePosition, const QAngle& eyeAngles )
{
	// UNDONE: Calc this on the server?  Disabled for now as it seems unnecessary to have this info on the server
#if defined( CLIENT_DLL )
	QAngle vmangoriginal = eyeAngles;
	QAngle vmangles = eyeAngles;
	Vector vmorigin = eyePosition;

	CBaseCombatWeapon *pWeapon = m_hWeapon.Get();
	//Allow weapon lagging
	//only if not in ironsight-mode
	if( pWeapon == NULL || !pWeapon->IsIronsighted() )
	{
		if ( pWeapon != NULL )
		{
	#if defined( CLIENT_DLL )
			if ( !prediction->InPrediction() )
	#endif
			{
				// add weapon-specific bob 
				pWeapon->AddViewmodelBob( this, vmorigin, vmangles );
			}
		}

		// Add model-specific bob even if no weapon associated (for head bob for off hand models)
		AddViewModelBob( owner, vmorigin, vmangles );

		// Add lag
		CalcViewModelLag( vmorigin, vmangles, vmangoriginal );

#if defined( CLIENT_DLL )
		if ( !prediction->InPrediction() )
		{
			// Let the viewmodel shake at about 10% of the amplitude of the player's view
			vieweffects->ApplyShake( vmorigin, vmangles, 0.1 );	
		}
#endif
	}

	CalcIronsights( vmorigin, vmangles );

	SetLocalOrigin( vmorigin );
	SetLocalAngles( vmangles );

#endif
}

baseviewmodel_shared.h

Don't forget to declare the new function:

 	void		CalcIronsights( Vector &pos, QAngle &ang );

Single Player Fix (Weapon model doesn't stay on ironsight)

In most cases weapon model (view model) dosen't stay in definied position when ironsight is toggled.

Fix is simply but requies few fixes (weapon blend to ironsight and blend out).

Go to basecombatweapon_shared.cpp

When you added two functions EnableIronsight and DisableIronsight. Change them to:

void CBaseCombatWeapon::EnableIronsights( void )
{
/*
#ifdef CLIENT_DLL
	if( !prediction->IsFirstTimePredicted() )
		return;
#endif*/
	if( !HasIronsights() || m_bIsIronsighted )
		return;
 
	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );
 
	if( !pOwner )
		return;
 
	if( pOwner->SetFOV( this, pOwner->GetDefaultFOV() + GetIronsightFOVOffset(), 0.4f ) ) //modify the last value to adjust how fast the fov is applied
	{
		m_bIsIronsighted = true;
		SetIronsightTime();
	}
}
 
void CBaseCombatWeapon::DisableIronsights( void )
{
/*
#ifdef CLIENT_DLL
	if( !prediction->IsFirstTimePredicted() )
		return;
#endif*/ 

// We are not using prediction in singleplayer


	if( !HasIronsights() || !m_bIsIronsighted )
		return;
 
	CBasePlayer *pOwner = ToBasePlayer( GetOwner() );
 
	if( !pOwner )
		return;
 
	if( pOwner->SetFOV( this, 0, 0.2f ) ) //modify the last value to adjust how fast the fov is applied
	{
		m_bIsIronsighted = false;
		SetIronsightTime();
	}
}


--Dexter127 03:43, 23 July 2012 (PDT)

Episode 1-engine fix

In the Episode 1-engine, SetFOV is server-side only. A simple fix would be to set the FOV on the server only, but here's a client-side implementation, partially derived from OB code.

m_hZoomOwner

Set m_hZoomOwner up for networking in player.cpp/.h by using a CNetworkHandle and adding it to the send-table.

In c_baseplayer.cpp/.h, add the m_hZoomOwner to the class-declaration, so it can be received. Set it to NULL in the constructor and add it to the recv- and the prediction-table.

Also add a SetFOV-function to C_BasePlayer and define it like this:

bool C_BasePlayer::SetFOV( C_BaseEntity *pRequester, int FOV, float zoomRate )
{
	//NOTENOTE: You MUST specify who is requesting the zoom change
	assert( pRequester != NULL );
	if ( pRequester == NULL )
		return false;

	if( ( m_hZoomOwner.Get() != NULL ) && ( m_hZoomOwner.Get() != pRequester ) )
		return false;
	else
	{
		//FIXME: Maybe do this is as an accessor instead
		if ( FOV == 0 )
		{
			m_hZoomOwner = NULL;
		}
		else
		{
			m_hZoomOwner = pRequester;
		}
	}

	m_iFOV = FOV;

	m_Local.m_flFOVRate	= zoomRate;

	return true;
}

ConVarRef

ConVarRef does not exist in the Episode 1-engine. You can either use the OB tier1-library (note: untested; might not work) or instead of ConVarRef( "sv_cheats" ), put extern ConVar sv_cheats; at the top of cbasecombatweapon_shared.cpp and use the member by pointer-operator (->) instead of the member-operator (.).

For the ConVars in the callbacks, simply use the ConVars directly or cast the IConVar to a ConVar.

Adjusting the bullet spread

Open the CPP of the weapon you want to be affected by ironsights. I used the SMG for this.

weapon_smg1.cpp

Search for the GetBulletSpread-function. It'll look something like this:

virtual const Vector& GetBulletSpread( void )
	{
			static const Vector cone = VECTOR_CONE_5DEGREES;
			return cone;
	}

You can, for example, replace it with the following:

virtual const Vector& GetBulletSpread( void )
	{
		if ( m_bIsIronsighted )
		{
			static const Vector cone = VECTOR_CONE_1DEGREES;
			return cone;
		}
		else
		{
			static const Vector cone = VECTOR_CONE_5DEGREES;
			return cone;
		}
	}

Here's an example picture with a 5 degree cone on the left, and a 1 degree cone on the right.

Bullet Spread Example

Add keybind

Last but not least, add the key to the Options/Keyboard-menu:

kb_act.lst

Probably somewhere in "#Valve_Combat_Title":

 "toggle_ironsight"		"#MOD_Toggle_Ironsight" 

And don't forget to add this #MOD_Toggle_Ironsight to your resource/MOD_english.txt (and the other lanuages).

Ironsight Sounds

basecombatweapon_shared.cpp

Add

pPlayer->EmitSound( "WWIPlayer.IronSightIn" );

to EnableIronsights and

pPlayer->EmitSound( "WWIPlayer.IronSightOut" );

to DisableIronsights.

Change the strings WWIPlayer.IronSightin and WWIPlayer.IronSightOut to your own sounds and precache them.


Prevent Ironsight while reloading

While reloading, you can still use Ironsight, even if you write DisableIronsights (); in bool CBaseCombatWeapon:: DefaultReload

Of course, it will turn off, but if you press the aim button again, Ironsight will start working and the reload animation will be played from the "aiming" view. It's not very pretty.

To prevent it, in:

basecombatweapon_shared.cpp

Find function: CBaseCombatWeapon::ToggleIronsights(void)


And completely replace it with:

void CBaseCombatWeapon::ToggleIronsights(void) //No possible use Iron Sight durin reloading
{
	if (m_bInReload == true)
	{
			DisableIronsights();
	}
	else
	{
		if (m_bIsIronsighted)
			DisableIronsights();
		else
			EnableIronsights();
	}
}


Simple Weapon Check and Show/Disable Crosshair

When you press ironsight toggle key with a weapon has no ironsight data in your hand.It will have no lag effect.And it also looks wired.

The crosshair also seems to be unnecessary when you toggle the ironsight view.I am sure that you will also want to hide it

The author of the article says use bool to avoid using the weapon that has no Ironsight data in its weapon-script,But It can be achieved without setting a bool.


CC_ToggleIronSights()

In the CC_ToggleIronSights,Use FClassnameIs() to set the weapon that allowed to use.

The functions will not work unless player has the weapons that set in FClassnameIs() in hand

#ifdef CLIENT_DLL
void CC_ToggleIronSights(void)
{
	

	CBasePlayer* pPlayer = C_BasePlayer::GetLocalPlayer();
	if (pPlayer == NULL)
		return;
	if (pPlayer->GetActiveWeapon() && (FClassnameIs(pPlayer->GetActiveWeapon(), "weapon_smg1") || FClassnameIs(pPlayer->GetActiveWeapon(), "weapon_pistol") || FClassnameIs(pPlayer->GetActiveWeapon(), "weapon_357") || 
         FClassnameIs(pPlayer->GetActiveWeapon(), "weapon_shotgun")))
	{
	
		//If Player is using the weapon that allowed to use(smg/pistol/357/shotgun),Toggle IronSights function:
		Msg("IronSight Mode Changed\n");//TestMsg
		pPlayer->GetActiveWeapon()->ToggleIronsights();//ToggleIronSights
		engine->ServerCmd("toggle_ironsight"); //forward to server
	
	}
	else//Otherwise
	{
		//Just Show a warning msg and do nothing special
		Warning("Curret Weapon Doesn`t allow to Use IronSight function!\n");
	}
	
	

	
}

static ConCommand toggle_ironsight("toggle_ironsight", CC_ToggleIronSights);
#endif

EnableIronsights()

Use function to get the ConVar that control crosshair show and hide,and give to the pointer and change to "0" to hide

void CBaseCombatWeapon::EnableIronsights(void)
{
	/*#ifdef CLIENT_DLL
		if (!prediction->IsFirstTimePredicted())
		return;
		#endif
		*/

	
	
			if (!HasIronsights() || m_bIsIronsighted)
				return;

			CBasePlayer *pOwner = ToBasePlayer(GetOwner());

			if (!pOwner)
				return;
	  
			if (pOwner->SetFOV(this, pOwner->GetDefaultFOV() + GetIronsightFOVOffset(), 0.4f)) //modify the last value to adjust how fast the fov is applied
			{
				//Set ConVar "crosshair" to 0 to hide crosshair	
				ConVar *crosshair = cvar->FindVar("Crosshair");
				crosshair->SetValue("0");
				m_bIsIronsighted = true;
				SetIronsightTime();
			}

	

}

DisableIronsights()

As same as above,but set to "1" to show it

void CBaseCombatWeapon::DisableIronsights(void)
{
	/*
	#ifdef CLIENT_DLL
	if( !prediction->IsFirstTimePredicted() )
	return;
	#endif
	*/
	// We are not using prediction in singleplayer
	


	

		if (!HasIronsights() || !m_bIsIronsighted)
			return;

		CBasePlayer *pOwner = ToBasePlayer(GetOwner());

		if (!pOwner)
			return;
	
			if (pOwner->SetFOV(this, 0, 0.2f)) //modify the last value to adjust how fast the fov is applied
			{
			//Set ConVar "crosshair" to 1 to show crosshair	
				ConVar *crosshair = cvar->FindVar("Crosshair");
				crosshair->SetValue("1");
				
				m_bIsIronsighted = false;
				SetIronsightTime();
			}
		
	
}


--233wer 16:52, 20 April 2023 (GMT)