PlayerSession.cs
using Sandbox;
using Sandbox.Services.Players;
using System;
using System.Linq;
public sealed class PlayerSession : Component
{
public static PlayerSession CurrentSession { get; private set; }
[Sync] public string AvatarUrl { get; set; }
[Sync] public string DisplayName { get; set; }
[Sync] public int Kills { get; set; }
[Sync] public int KillStreak { get; set; }
[Sync] public int Deaths { get; set; }
[Sync] public bool IsVR { get; set; }
/// <summary>Index into the current map vote options, or -1 if the player has not voted.</summary>
[Sync] public int MapVote { get; set; } = -1;
/// <summary>Standing eye height in game units (inches), from VR calibration or the desktop default.</summary>
[Sync] public float StandingEyeHeight { get; set; } = 64f;
/// <summary>True after the owner finishes join setup (desktop skip, or VR Continue).</summary>
[Sync, Change( nameof( OnReadyToSpawnChanged ) )]
public bool IsReadyToSpawn { get; set; }
/// <summary>True after the owner has pressed Calibrate at least once this session.</summary>
[Sync] public bool HasCalibratedHeight { get; set; }
/// <summary>True when a spawn transform was reserved during VR calibration.</summary>
[Sync] public bool HasReservedSpawn { get; set; }
/// <summary>World position reserved at calibration; reused when the real pawn spawns.</summary>
[Sync] public Vector3 ReservedSpawnPosition { get; set; }
/// <summary>World rotation reserved at calibration; reused when the real pawn spawns.</summary>
[Sync] public Rotation ReservedSpawnRotation { get; set; } = Rotation.Identity;
public Guid LastKiller { get; set; }
private float _lastKillTime;
private int _multiKillCount;
private GameObject _calibrationRigGo;
private VrCalibrationRig _calibrationRig;
private bool _wantsCalibrationRig;
private const float MinEyeHeight = 40f;
private const float MaxEyeHeight = 80f;
private const string VrRootPrefabPath = "player/vrroot.prefab";
public int RegisterMultiKill( float windowSeconds )
{
var now = Time.Now;
if ( _lastKillTime > 0f && (now - _lastKillTime) <= windowSeconds )
_multiKillCount++;
else
_multiKillCount = 1;
_lastKillTime = now;
return _multiKillCount;
}
public void ResetKillStreaks()
{
KillStreak = 0;
_multiKillCount = 0;
_lastKillTime = 0f;
}
/// <summary>Samples the VR headset height and stores it as standing eye height.</summary>
public float CalibrateStandingEyeHeight()
{
if ( IsProxy || !Game.IsRunningInVR )
return StandingEyeHeight;
var height = _calibrationRig.IsValid()
? _calibrationRig.MeasureEyeHeight()
: Input.VR.Head.Position.z;
StandingEyeHeight = Math.Clamp( height, MinEyeHeight, MaxEyeHeight );
HasCalibratedHeight = true;
return StandingEyeHeight;
}
/// <summary>Marks the session ready to spawn after a successful calibration.</summary>
public bool ConfirmCalibration()
{
if ( IsProxy || !HasCalibratedHeight )
return false;
DestroyCalibrationRig();
IsReadyToSpawn = true;
return true;
}
public static string FormatHeightFeet( float inches )
{
var totalInches = (int)MathF.Round( inches );
var feet = totalInches / 12;
var remainingInches = totalInches % 12;
return $"{feet}'{remainingInches}\"";
}
public static string FormatHeightCentimeters( float inches )
{
var cm = MathF.Round( inches * 2.54f );
return $"{cm:0} cm";
}
protected override void OnStart()
{
NetworkManager.Instance.PlayerSessions.Add( this );
if ( !IsProxy )
{
AvatarUrl = Profile.Local.Avatar;
DisplayName = Profile.Local.Name;
CurrentSession = this;
IsVR = Game.IsRunningInVR;
if ( !Game.IsRunningInVR || NetworkManager.Instance.SkipHeightCalibration )
{
StandingEyeHeight = 64f;
IsReadyToSpawn = true;
}
else
{
_wantsCalibrationRig = true;
Log.Info( "PlayerSession: VR client waiting to create calibration rig." );
}
}
}
protected override void OnUpdate()
{
if ( !_wantsCalibrationRig || IsReadyToSpawn || _calibrationRigGo.IsValid() )
return;
if ( NetworkManager.Instance is null || !NetworkManager.Instance.IsMapReady )
return;
CreateCalibrationRig();
}
protected override void OnDestroy()
{
DestroyCalibrationRig();
if ( CurrentSession == this )
CurrentSession = null;
NetworkManager.Instance?.PlayerSessions.Remove( this );
}
private void OnReadyToSpawnChanged( bool oldValue, bool newValue )
{
if ( !newValue || !Networking.IsHost )
return;
NetworkManager.Instance?.TrySpawnPlayer( Network.Owner );
}
private void CreateCalibrationRig()
{
if ( _calibrationRigGo.IsValid() )
return;
var spawnTransform = PickSpawnTransform();
ReservedSpawnPosition = spawnTransform.Position;
ReservedSpawnRotation = spawnTransform.Rotation;
HasReservedSpawn = true;
_calibrationRigGo = new GameObject( "VrCalibrationRig" )
{
NetworkMode = NetworkMode.Never,
WorldTransform = spawnTransform
};
_calibrationRig = _calibrationRigGo.AddComponent<VrCalibrationRig>();
_calibrationRig.Session = this;
var vrRoot = CloneVrRoot( _calibrationRigGo );
if ( !vrRoot.IsValid() )
{
Log.Warning( "PlayerSession: failed to clone VR root for calibration; will retry." );
_calibrationRigGo.Destroy();
_calibrationRigGo = null;
_calibrationRig = null;
return;
}
var vrController = vrRoot.Components.Get<VrCharacterController>( FindMode.EverythingInSelfAndDescendants );
if ( vrController.IsValid() )
vrController.MovementEnabled = false;
_calibrationRig.EnsurePanel();
_wantsCalibrationRig = false;
Log.Info( $"PlayerSession: calibration rig created at {spawnTransform.Position}." );
}
private static GameObject CloneVrRoot( GameObject parent )
{
var fromPlayerPrefab = NetworkManager.Instance?.PlayerPrefab?
.Components.Get<PlayerCharacterController>()?.VRRootPrefab;
if ( fromPlayerPrefab.IsValid() )
return fromPlayerPrefab.Clone( transform: global::Transform.Zero, parent: parent );
try
{
return GameObject.Clone( VrRootPrefabPath, global::Transform.Zero, parent );
}
catch ( Exception e )
{
Log.Warning( $"PlayerSession: GameObject.Clone(\"{VrRootPrefabPath}\") failed: {e.Message}" );
return null;
}
}
private void DestroyCalibrationRig()
{
_wantsCalibrationRig = false;
if ( _calibrationRigGo.IsValid() )
_calibrationRigGo.Destroy();
_calibrationRigGo = null;
_calibrationRig = null;
}
private static Transform PickSpawnTransform()
{
var spawnPoints = Game.ActiveScene.GetAllComponents<SpawnPoint>().ToArray();
if ( spawnPoints.Length == 0 )
return global::Transform.Zero;
return spawnPoints[Game.Random.Int( 0, spawnPoints.Length - 1 )].Transform.World;
}
public void ClearStats()
{
Kills = 0;
KillStreak = 0;
Deaths = 0;
LastKiller = Guid.Empty;
ResetKillStreaks();
}
}