Phone/RpVoice.cs

A Voice component for Horizon RP, placed on the player prefab. Chooses between proximity voice and phone call audio: the listener machine switches the voice to non-positional when in a call, and the speaker's own machine keeps the mic open during calls or while holding the voice key.

Networking
namespace HorizonRP;

/// <summary>
/// La voix d'un joueur, posée sur son prefab à la place du composant Voice.
///
/// Le moteur n'enregistre qu'une voix à la fois par joueur : la proximité et le
/// téléphone sont donc UNE seule voix, dont on change le rendu.
/// - Sur la machine de l'auditeur : si l'auditeur est en appel avec ce joueur,
///   la voix est jouée « dans les oreilles » (sans position 3D) ; sinon, en 3D,
///   comme une voix normale qu'on n'entend que de près.
/// - Sur la machine du joueur lui-même : en appel, le micro reste ouvert (comme
///   un vrai téléphone) ; sinon, on parle en maintenant la touche V.
/// </summary>
[Title( "Horizon RP – Voix" )]
[Icon( "record_voice_over" )]
public sealed class RpVoice : Voice
{
	/// <summary>Portée de la voix de proximité (unités s&box, ~25 m).</summary>
	const float ProximityRange = 1000f;

	protected override void OnFixedUpdate()
	{
		var speaker = GetComponent<RoleplayPlayer>();
		var phone = PhoneSystem.Current;
		if ( speaker is null || phone is null )
			return;

		if ( !IsProxy )
		{
			// L'écoute est décidée ici (mode Manuel) plutôt que par l'appuyer-pour-parler du
			// moteur : celui-ci lit l'état de l'action « voice », qui restait enfoncée quand la
			// touche était relâchée pendant qu'une zone de saisie avait le clavier (téléphone,
			// menu). Le micro restait alors ouvert, notre anneau figé et le témoin de Steam
			// allumé (Julien, 29/10/2026). On lit la vraie touche du clavier (celle choisie dans
			// Keybinds), et jamais pendant une saisie de texte.
			// Un appel décroché, n'importe lequel : CallOf rend le premier appel du joueur, et un
			// ancien (sonnerie, refus) resté devant laissait le micro fermé — il fallait V pour être
			// entendu en appel (Julien, 30/10/2026). Le même test que Talking, côté auditeur.
			var inCall = phone.Calls.Any( x => x.Active && x.Involves( speaker.SteamId ) );
			var key = Keybinds.KeyOf( "Voice" );
			var pushing = !string.IsNullOrEmpty( key ) && Input.Keyboard.Down( key )
				&& Sandbox.UI.InputFocus.Current is null;
			if ( Mode != ActivateMode.Manual )
				Mode = ActivateMode.Manual;
			var listen = inCall || pushing;
			if ( IsListening != listen )
				IsListening = listen;
			return;
		}

		var listener = RoleplayPlayer.Local;
		var onThePhone = listener is not null && phone.Talking( speaker.SteamId, listener.SteamId );
		WorldspacePlayback = !onThePhone;
		Distance = ProximityRange;
	}

	/// <summary>
	/// Hors appel, on n'envoie sa voix qu'aux joueurs à portée : inutile de
	/// charger le réseau pour quelqu'un à l'autre bout de la ville.
	/// </summary>
	protected override IEnumerable<Connection> ExcludeFilter()
	{
		var speaker = GetComponent<RoleplayPlayer>();
		var phone = PhoneSystem.Current;
		if ( speaker is null || phone is null )
			return Enumerable.Empty<Connection>();

		// Dans la ruelle de la création, personne ne s'entend : on n'y est pas encore en jeu.
		return RoleplayPlayer.All
			.Where( x => x != speaker
				&& ((x.WorldPosition.Distance( speaker.WorldPosition ) > ProximityRange * 1.2f && !phone.Talking( speaker.SteamId, x.SteamId ))
					|| x.NeedsCharacter || speaker.NeedsCharacter) )
			.Select( x => x.Network.Owner )
			.Where( x => x is not null )
			.ToList();
	}
}