things/player/Player.Lobby.cs

Partial Player class extension that manages lobby behavior and animations. It syncs lobby state and look offsets, updates player position/rotation to a lobby slot, drives the animation helper for idle lobby pose, provides a debug console command and debug overlay, and contains EnterLobby RPC to initialize lobby state.

Networking
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Diagnostics;
using System;

public partial class Player
{
	[Sync( SyncFlags.FromHost )] public int LobbySlotIndex { get; set; } = -1;
	[Sync] public Vector2 LobbyLookMouseOffset { get; set; }
	[Sync] public bool IsInLobby { get; set; }
	private Vector2 _displayLobbyLookMouseOffset;

	public static bool DebugLobbyAnim { get; set; }

	[ConCmd( "debug_lobby_anim" )]
	public static void ToggleDebugLobbyAnim()
	{
		DebugLobbyAnim = !DebugLobbyAnim;
		Log.Info( $"debug_lobby_anim: {DebugLobbyAnim}" );
	}

	public void UpdateLobby()
	{
		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"IsNoclipping: {AnimationHelper.IsNoclipping}", new global::Transform( WorldPosition + Vector3.Up * 10f ) );

		var container = Manager.Instance.CameraContainer;
		var cameraPos = container.WorldPosition;

		var associatedSlot = Scene.GetAllComponents<LobbyPlayerSlot>().FirstOrDefault( x => x.SlotIndex == LobbySlotIndex );
		if ( associatedSlot != null )
		{
			WorldPosition = associatedSlot.WorldPosition;
			var yaw = Rotation.LookAt( cameraPos - WorldPosition, Vector3.Up ).Yaw();
			Model.LocalRotation = new Angles( 0f, yaw, 0f );
		}

		if ( IsProxy )
		{
			if ( _displayLobbyLookMouseOffset.LengthSquared <= 0.0001f )
				_displayLobbyLookMouseOffset = LobbyLookMouseOffset;

			_displayLobbyLookMouseOffset = Vector2.Lerp( _displayLobbyLookMouseOffset, LobbyLookMouseOffset, RealTime.Delta * 16f );
		}
		else
		{
			_displayLobbyLookMouseOffset = LobbyLookMouseOffset;
		}

		var lookPos = cameraPos + new Vector3( 0f, -_displayLobbyLookMouseOffset.x, _displayLobbyLookMouseOffset.y ) * 200f;
		AnimationHelper.WithLook( lookPos - (WorldPosition + Vector3.Up * 60f) );

		AnimationHelper.IsNoclipping = false;
		AnimationHelper.IsGrounded = true; // set every frame, setting once in EnterLobby can be lost before the animgraph is ready in published builds
		if ( _swapAnimTimeRemaining <= 0f )
			AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.Shotgun;
		AnimationHelper.WithVelocity( Vector3.Zero );
		AnimationHelper.WithWishVelocity( Vector3.Zero );
		AnimationHelper.Target.Set( "scale_height", 1f ); // not sure why this is necessary, but when game first starts the player has 1.2 height in lobby

		if ( IsProxy )
			return;

		LobbyLookMouseOffset = new Vector2(Utils.Map(Mouse.Position.x, 0f, Screen.Width, -1f, 1f), Utils.Map( Mouse.Position.y, 0f, Screen.Height, 1f, -1f ) );
		_displayLobbyLookMouseOffset = LobbyLookMouseOffset;

		if ( DebugLobbyAnim )
			DrawLobbyAnimDebug();
	}

	void DrawLobbyAnimDebug()
	{
		var t = AnimationHelper.Target;
		if ( !t.IsValid() )
		{
			DebugOverlay.ScreenText( new Vector2( 20f, 200f ), "AnimationHelper.Target is null", flags: TextFlag.LeftTop );
			return;
		}

		var graph = t.SceneModel?.AnimationGraph;
		var groundedLookup = "no graph";
		if ( graph.IsValid() )
		{
			groundedLookup = graph.TryGetParameterIndex( "b_grounded", out var idx )
				? $"idx {idx} name '{graph.GetParameterName( idx )}' type {graph.GetParameterType( "b_grounded" )?.Name}"
				: $"NOT FOUND (type {graph.GetParameterType( "b_grounded" )?.Name})";
			groundedLookup += $"  params {graph.ParamCount}";
		}

		var renderers = Model.GetComponentsInChildren<SkinnedModelRenderer>();
		var text = $"grounded {t.GetBool( "b_grounded" )}  noclip {t.GetBool( "b_noclip" )}  swim {t.GetBool( "b_swim" )}  sit {t.GetBool( "b_sit" )}\n" +
			$"b_grounded lookup: {groundedLookup}\n" +
			$"holdtype {t.GetInt( "holdtype" )}  heel {t.GetFloat( "scale_heel" )}  height {t.GetFloat( "scale_height" )}  move_z {t.GetFloat( "move_z" )}\n" +
			$"target model {t.Model?.ResourcePath}  go '{t.GameObject.Name}'\n" +
			$"graph {t.SceneModel?.AnimationGraph?.ResourcePath}  sceneModel {t.SceneModel.IsValid()}  useGraph {t.UseAnimGraph}\n" +
			$"renderers under Model: {renderers.Count()}  first {renderers.FirstOrDefault()?.GameObject.Name}\n" +
			$"z {WorldPosition.z}  modelLocal {Model.LocalPosition}  IsInLobby {IsInLobby}";

		DebugOverlay.ScreenText( new Vector2( 20f, 200f ), text, flags: TextFlag.LeftTop );
	}

	[Rpc.Broadcast( NetFlags.Reliable )]
	public void EnterLobby()
	{
		IsInLobby = true;

		var container = Manager.Instance.CameraContainer;
		container.WorldPosition = Manager.LOBBY_CAMERA_POS;
		container.WorldRotation = new Angles( 0f, 0f, 0f );

		PhysicalCollider.Enabled = true;

		AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.Shotgun;
		AnimationHelper.IsNoclipping = false;
		AnimationHelper.IsGrounded = true;
		SetGunVisible( true );

		//if ( IsProxy )
		//	return;

		//SetLobbyHoldType( GetRandomLobbyHoldType() );
	}

	//public CitizenAnimationHelper.HoldTypes GetRandomLobbyHoldType()
	//{
	//	return CitizenAnimationHelper.HoldTypes.Shotgun;
	//}

	//[Rpc.Broadcast(NetFlags.Reliable)]
	//public void SetLobbyHoldType( CitizenAnimationHelper.HoldTypes holdType)
	//{
	//	AnimationHelper.HoldType = holdType;
	//}
}