🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (420 total matches found)
softsplit.sandbox / ui/controls/color/colorcontrol.cs.scss
Game game
ColorControl
{
	gap: 0.5rem;
	flex-grow: 1;
	pointer-events: all;
	background-color: #000a;
	border-radius: 4px;
	padding: 2px;
	height: 32px;
}

ColorControl TextEntry
{
	flex-grow: 1;
	flex-shrink: 0;
	color: #aaa;
	font-size: 1.2rem;

	&:hover, &:focus
	{
		color: #ddd;

		.icon
		{
			color: #3af;
		}
	}

	&:active
	{
		color: white;
	}
}

ColorControl > .colorswatch
{
	aspect-ratio: 1;
	border-radius: 4px;
	height: 100%;
	flex-shrink: 0;
	cursor: pointer;
	border: 2px solid #000;
}
softsplit.sandbox / ui/contextmenu/componenthandle.cs.scss
Game game
ComponentHandle
{
	width: 38px;
	height: 38px;
	background-color: #eee;
	box-shadow: 2px 2px 16px black;
	border-radius: 20px;
	position: absolute;
	justify-content: center;
	align-items: center;
	cursor: pointer;
	transform: scale( 1 ) translateX( -50% ) translateY( -50% );
	z-index: 100;

	.icon
	{
		font-family: "Material Icons";
		color: #08f;
		font-size: 23px;
		pointer-events: none;
	}

	&:hover
	{
		opacity: 1;
		background-color: #fff;
	}

	&:active
	{
		border: 1px solid #08f;
		background-color: #fff;
		opacity: 1;
		transform: scale( 0.95 ) translateX( -50% ) translateY( -50% );
	}

	&.active
	{
		border: 2px solid #fff;
		background-color: #08f;
		opacity: 1;

		.icon
		{
			color: #fff;
		}
	}

	&:outro
	{
		transform: scale( 0 ) translateX( -50% ) translateY( -50% );
		transition: transform 0.2s ease-out;
	}
}
softsplit.sandbox / ui/controlsheet/controlsheetrow.cs.scss
Game game
ControlSheetRow
{
	flex-direction: row;
	flex-shrink: 0;
	max-height: 32px;
	border-radius: 4px;

	&.hidden
	{
		display: none;
	}
}

ControlSheetRow > .left
{
	width: 150px;
	flex-shrink: 0;
	flex-grow: 0;
	padding: 6px;
}

ControlSheetRow > .right
{
	flex-grow: 1;
}

ControlSheetRow > .left > .title
{
	width: 150px;
	flex-shrink: 0;
	flex-grow: 0;
	font-size: 13px;
	white-space: nowrap;
	overflow: hidden;
}

ControlSheetRow > .right > textentry,
ControlSheetRow > .right numberentry
{
	background-color: #000a;
	flex-grow: 1;
	padding: 4px 8px;
	border-radius: 2px;
}

softsplit.sandbox / ui/pressable/pressablehud.razor.scss
Game game
@import "/UI/Theme.scss";

PressableHud
{
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	justify-content: center;
	align-items: center;
	font-family: $body-font;
	font-size: 14px;
	font-weight: 600;
	z-index: 1000;
	opacity: 0.2;

	.crosshair
	{
		width: 12px;
		height: 12px;
		background-color: white;
		border-radius: 50%;
		box-shadow: 0px 0px 8px black;
		opacity: 0;
		transition: all 0.1s linear;
	}

	&.active
	{
		opacity: 1;

		.crosshair
		{
			opacity: 1;
		}
	}
}
softsplit.sandbox / ui/theme.scss
Game game
$deadzone-x: 2vw;
$deadzone-y: 2vw;

$color-accent: #0094ff;
$color-text: #fafaff;

$title-font: "Poppins";
$body-font: "Inter";
$subtitle-font: "Poppins";

@import "/UI/Menu.scss";
@import "/UI/Hud.scss";
softsplit.sandbox / ui/faceposer/faceposeeditor.razor.scss
Game game
@import "/UI/Theme.scss";

FacePoseEditor
{
    flex-direction: column;
    width: 100%;
    max-height: 400px;
}

FacePoseEditor .group-header
{
    font-size: 11px;
    font-weight: 700;
    color: rgba( 255, 255, 255, 0.35 );
    padding: 6px 6px 2px 6px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    flex-shrink: 0;
}
softsplit.sandbox / ui/menu.scss
Game game
$menu-color:#cdf;
$menu-text: #fffa;
$menu-text-strong: #fff;
$menu-surface-soft: #0004;
$menu-accent: $color-accent;
$menu-accent-soft: rgba($color-accent, 0.13);
$menu-panel-bg: #0c202fcc;

.menu-header-bar
{
	border-radius: 8px 8px 0 0;
	z-index: 100;
	font-family: $title-font;
	font-weight: 600;
}

.menu-input
{
	background-color: $menu-surface-soft;
	border-radius: 8px;
	padding: 2px 10px;
}

.menu-segmented-group
{
	background-color: #0008;
	border-radius: 8px;
	overflow: visible;
}

.menu-mode-button
{
	color: $menu-text;
	font-size: 1rem;
	font-weight: 600;
	padding: 1rem;
	font-family: $subtitle-font;
	cursor: pointer;
	pointer-events: all;
	justify-content: center;
	align-items: center;
	overflow: visible;
	border-radius: 8px;

	.icon
	{
		margin-left: -4px;
		margin-top: -1.5px;
		padding-right: 4px;
		font-size: 1.5rem;
	}

	.title
	{
		font-size: 1.1rem;
		font-weight: 700;
	}

	&:hover
	{
		color: $menu-text-strong;
	}

	&.active
	{
		outline: 1px solid $menu-accent;
		background-color: rgba( $menu-accent, 0.2 );
		color: $menu-text-strong;
	}
}

.menu-icon-toggle-group
{
	background-color: $menu-surface-soft;
	border-radius: 8px;
	gap: 0.5rem;
	padding: 4px 4px;
	padding-left: 8px;
	align-items: center;

	IconPanel
	{
		width: 20px;
		aspect-ratio: 1;
		opacity: 0.35;
		align-items: center;
		cursor: pointer;

		&:hover
		{
			opacity: 0.7;
		}

		&.active
		{
			opacity: 1;
		}
	}
}

span
{
	&.grow
	{
		gap: 8px;

		> *
		{
			flex-grow: 1;
		}
	}
}

.menu-action
{
	color: $menu-text-strong;
	border: 1px solid transparent;
	border-radius: 4px;
	cursor: pointer;
	padding: 6px 10px;
	align-items: center;
	justify-content: center;
	gap: 0.5rem;
	font-weight: 600;

	label, .title
	{
		font-size: 14px;
	}

	IconPanel, .icon
	{
		font-size: 1.2rem;
	}

	&.disabled
	{
		pointer-events: none;
		opacity: 0.25;
	}

	&.soft
	{
		background-color: $menu-accent-soft;

		&:hover
		{
			background-color: rgba($color-accent, 0.4);
			sound-in: ui.button.over;
		}
	}

	&.primary
	{
		border-color: $menu-accent;

		&:hover
		{
			background-color: $menu-accent-soft;
			sound-in: ui.button.over;
		}
	}

	&.wide
	{
		width: 100%;
	}
}

.menu-filter-input
{
	width: 200px;
}

TextEntry 
{
	color: $color-text;
	font-family: $body-font;
}
softsplit.sandbox / Cleanup/CleanupSystem.cs
Game game
using Sandbox.UI;

namespace Sandbox;

public interface ICleanupEvents
{
	public void OnCleanup( int removedObjects, int restoredObjects );
}

/// <summary>
/// A system that tracks the baseline scene state and allows resetting the map to its original state.
/// Removes all spawned props and restores destroyed map objects while leaving players untouched.
/// </summary>
internal sealed class CleanupSystem : GameObjectSystem<CleanupSystem>, ISceneLoadingEvents
{
	/// <summary>
	/// Set of GameObjects that existed in the original scene baseline.
	/// </summary>
	private readonly HashSet<Guid> _baselineObjectIds = new();

	/// <summary>
	/// Serialized data of baseline objects so we can restore them if destroyed.
	/// </summary>
	private readonly Dictionary<Guid, string> _baselineObjectData = new();

	private static bool _restorePersistedBaseline;
	private static HashSet<Guid> _persistedBaselineIds;
	private static Dictionary<Guid, string> _persistedBaselineData;

	/// <summary>
	/// Whether a baseline has been captured.
	/// </summary>
	public bool HasBaseline => _baselineObjectIds.Count > 0;

	public CleanupSystem( Scene scene ) : base( scene )
	{
	}

	/// <summary>
	/// Call from SaveSystem before Game.ChangeScene() to snapshot the current baseline
	/// </summary>
	public static void PreserveBaselineForSaveLoad()
	{
		if ( Current is null || !Current.HasBaseline ) return;

		_restorePersistedBaseline = true;
		_persistedBaselineIds = new HashSet<Guid>( Current._baselineObjectIds );
		_persistedBaselineData = new Dictionary<Guid, string>( Current._baselineObjectData );
	}

	void ISceneLoadingEvents.BeforeLoad( Scene scene, SceneLoadOptions options )
	{
		// Clear any existing baseline when a new scene is loading
		_baselineObjectIds.Clear();
		_baselineObjectData.Clear();
	}

	async Task ISceneLoadingEvents.OnLoad( Scene scene, SceneLoadOptions options, LoadingContext context )
	{
		// We don't care if the game is not playing
		if ( !Game.IsPlaying ) return;

		// Wait for next frame to ensure all objects are spawned
		await Task.Yield();

		// Could be null if the scene was unloaded before this runs
		if ( !Scene.IsValid() ) return;

		// When loading a save, restore the baseline captured before the scene was destroyed
		if ( _restorePersistedBaseline && _persistedBaselineIds is not null )
		{
			_baselineObjectIds.UnionWith( _persistedBaselineIds );
			foreach ( var kvp in _persistedBaselineData )
				_baselineObjectData.TryAdd( kvp.Key, kvp.Value );

			_restorePersistedBaseline = false;
			Log.Info( $"CleanupSystem: Restored persisted baseline with {_baselineObjectIds.Count} objects." );
		}
		else
		{
			CaptureBaseline();
		}
	}

	/// <summary>
	/// Captures the current scene state as the baseline.
	/// All objects that exist at this point are considered part of the original map.
	/// </summary>
	public void CaptureBaseline()
	{
		_baselineObjectIds.Clear();
		_baselineObjectData.Clear();

		foreach ( var go in Scene.Children?.ToArray() ?? [] )
		{
			CaptureObjectRecursive( go );
		}

		Log.Info( $"CleanupSystem: Captured baseline with {_baselineObjectIds.Count} objects." );
	}

	private void CaptureObjectRecursive( GameObject go )
	{
		if ( !go.IsValid() )
			return;

		// Skip player objects
		if ( IsPlayerObject( go ) )
			return;

		if ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )
			return;

		_baselineObjectIds.Add( go.Id );

		var serialized = go.Serialize();
		if ( serialized is not null )
		{
			_baselineObjectData[go.Id] = serialized.ToJsonString();
		}

		foreach ( var child in go.Children?.ToArray() ?? [] )
		{
			CaptureObjectRecursive( child );
		}
	}

	/// <summary>
	/// Determines if a GameObject is a player or belongs to a player.
	/// </summary>
	private static bool IsPlayerObject( GameObject go )
	{
		if ( !go.IsValid() )
			return false;

		if ( go.Components.Get<Player>( true ) is not null )
			return true;

		if ( go.Components.Get<PlayerData>( true ) is not null )
			return true;

		var parent = go.Parent;
		while ( parent is not null && parent != go.Scene )
		{
			if ( parent.Components.Get<Player>( true ) is not null )
				return true;
			if ( parent.Components.Get<PlayerData>( true ) is not null )
				return true;
			parent = parent.Parent;
		}

		return false;
	}

	/// <summary>
	/// Cleans up the scene by removing all spawned objects and restoring destroyed baseline objects.
	/// Players and their belongings are preserved.
	/// </summary>
	public void Cleanup()
	{
		if ( !HasBaseline )
		{
			Log.Warning( "CleanupSystem: No baseline captured. Cannot cleanup." );
			return;
		}

		if ( !Networking.IsHost )
		{
			Log.Warning( "CleanupSystem: Only the host can perform cleanup." );
			return;
		}

		var removedCount = 0;
		var restoredCount = 0;
		var objectsToRemove = new List<GameObject>();
		var existingBaselineIds = new HashSet<Guid>();

		foreach ( var go in Scene.GetAllObjects( true ) )
		{
			if ( !go.IsValid() )
				continue;

			// Never remove player objects
			if ( IsPlayerObject( go ) )
				continue;

			if ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )
				continue;

			if ( _baselineObjectIds.Contains( go.Id ) )
			{
				existingBaselineIds.Add( go.Id );
			}
			else
			{
				if ( go.Parent == Scene )
				{
					objectsToRemove.Add( go );
				}
			}
		}

		// Remove spawned objects
		foreach ( var go in objectsToRemove )
		{
			if ( go.IsValid() )
			{
				go.Destroy();
				removedCount++;
			}
		}

		// Restore destroyed baseline objects
		foreach ( var kvp in _baselineObjectData )
		{
			var id = kvp.Key;

			// Skip if the object still exists
			if ( existingBaselineIds.Contains( id ) )
				continue;

			// Skip if we already processed the parent object
			var go = Scene.Directory.FindByGuid( id );
			if ( go.IsValid() )
				continue;

			try
			{
				var json = System.Text.Json.Nodes.JsonNode.Parse( kvp.Value );
				if ( json is System.Text.Json.Nodes.JsonObject jso )
				{
					var restored = new GameObject();
					restored.Deserialize( jso );
					restoredCount++;
				}
			}
			catch ( System.Exception ex )
			{
				Log.Warning( $"CleanupSystem: Failed to restore object {id}: {ex.Message}" );
			}
		}

		BroadcastCleanup( removedCount, restoredCount );
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	private static void BroadcastCleanup( int removedObjects, int restoredObjects )
	{
		Game.ActiveScene?.RunEvent<ICleanupEvents>( x => x.OnCleanup( removedObjects, restoredObjects ) );

		Log.Info( $"Cleanup complete. Removed {removedObjects} spawned objects, restored {restoredObjects} destroyed objects." );
	}

	/// <summary>
	/// Console command to cleanup the map.
	/// </summary>
	[ConCmd( "cleanup" )]
	public static void CleanupCommand( string targetName = null )
	{
		if ( !Networking.IsHost ) return;

		//
		// Targeted cleanup, doesn't use the same cleanup shit
		//
		if ( !string.IsNullOrEmpty( targetName ) )
		{
			var target = GameManager.FindPlayerWithName( targetName );
			if ( target is not null )
			{
				CleanupPlayer( target );
			}
			else
			{
				Notices.AddNotice( "cleaning_services", Color.Red, $"Can't find {targetName} to clean up" );
			}

			return;
		}

		if ( Current is null )
		{
			Log.Warning( "CleanupSystem: No active cleanup system." );
			return;
		}

		Current.Cleanup();
	}

	[Rpc.Host]
	public static void RpcCleanUpMine()
	{
		CleanupPlayer( Rpc.Caller );
	}

	[Rpc.Host]
	public static void RpcCleanUpAll()
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		Current?.Cleanup();
	}

	[Rpc.Host]
	public static void RpcCleanUpTarget( Connection target )
	{
		if ( !Rpc.Caller.HasPermission( "admin" ) ) return;

		CleanupPlayer( target );
	}

	public static void CleanupPlayer( Connection caller )
	{
		Assert.True( Networking.IsHost, "Only the host may call this method!" );

		var removable = Game.ActiveScene.GetAllComponents<Ownable>()
			.Where( o => o.Owner == caller );

		var count = 0;
		foreach ( var ownable in removable.ToArray() )
		{
			ownable.GameObject.Destroy();
			count++;
		}

		Notices.SendNotice( caller, "cleaning_services", Color.Green, $"Cleaned up {count} objects" );
	}

}
softsplit.sandbox / Game/Entity/DynamiteEntity.cs
Game game
[Alias( "dynamite" )]
public sealed class DynamiteEntity : Component, IPlayerControllable, Component.IDamageable
{
	[Property, Range( 1, 500 ), Step( 1 ), ClientEditable]
	public float Damage { get; set; } = 128;

	[Property, Range( 16, 4096 ), Step( 16 ), ClientEditable]
	public float Radius { get; set; } = 1024f;

	[Property, Range( 1, 100 ), Step( 1 ), ClientEditable]
	public float Force { get; set; } = 1;

	[Property, Sync, ClientEditable]
	public ClientInput Activate { get; set; }

	bool _isDead = false;

	[Rpc.Host]
	public void Explode()
	{
		_isDead = true;

		var explosionPrefab = ResourceLibrary.Get<PrefabFile>( "/prefabs/engine/explosion_med.prefab" );
		if ( explosionPrefab == null )
		{
			Log.Warning( "Can't find /prefabs/engine/explosion_med.prefab" );
			return;
		}

		var go = GameObject.Clone( explosionPrefab, new CloneConfig { Transform = WorldTransform.WithScale( 1 ), StartEnabled = false } );
		if ( !go.IsValid() ) return;

		go.RunEvent<RadiusDamage>( x =>
		{
			x.Radius = Radius;
			x.PhysicsForceScale = Force;
			x.DamageAmount = Damage;
			x.Attacker = go;
		}, FindMode.EverythingInSelfAndDescendants );

		go.Enabled = true;
		go.NetworkSpawn( true, null );

		GameObject.Destroy();
	}

	void IDamageable.OnDamage( in DamageInfo damage )
	{
		if ( _isDead ) return;
		if ( IsProxy ) return;

		Explode();
	}

	void IPlayerControllable.OnControl()
	{
		if ( Activate.Pressed() )
		{
			Explode();
		}
	}

	void IPlayerControllable.OnEndControl()
	{
		// nothing to do
	}

	void IPlayerControllable.OnStartControl()
	{
		// nothing to do
	}
}
softsplit.sandbox / Game/Entity/EmitterEntity.cs
Game game
/// <summary>
/// Whether the emitter fires while the input is held, or toggles on/off with a press.
/// </summary>
public enum EmitMode
{
	/// <summary>
	/// Press once to turn on, press again to turn off.
	/// </summary>
	Toggle,
	/// <summary>
	/// Emits only while the input is held down.
	/// </summary>
	Hold,
}

/// <summary>
/// A world-placed SENT that spawns and controls a particle/VFX emitter.
/// The emitter prefab is defined by a <see cref="ScriptedEmitter"/> resource.
/// </summary>
[Alias( "emitter" )]
public sealed class EmitterEntity : Component, IPlayerControllable
{
	/// <summary>
	/// The emitter definition points to a prefab containing a particle system.
	/// </summary>
	[Property, ClientEditable]
	public ScriptedEmitter Emitter { get; set; }

	/// <summary>
	/// Whether this emitter toggles on/off with a press, or emits only while held.
	/// </summary>
	[Property, ClientEditable]
	public EmitMode Mode { get; set; } = EmitMode.Toggle;

	/// <summary>
	/// Used when <see cref="Mode"/> is <see cref="EmitMode.Toggle"/>.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput ToggleInput { get; set; }

	/// <summary>
	/// Used when <see cref="Mode"/> is <see cref="EmitMode.Hold"/>.
	/// </summary>
	[Property, Sync, ClientEditable, Group( "Input" )]
	public ClientInput HoldInput { get; set; }

	/// <summary>
	/// Whether the emitter is currently active. Synced to all clients.
	/// </summary>
	[Sync] public bool IsEmitting { get; private set; }

	/// <summary>
	/// When enabled, forces the emitter on regardless of input or mode.
	/// Can be set from the editor or wired up externally.
	/// </summary>
	[Property, ClientEditable]
	public bool ManualOn
	{
		get => _manualOn;
		set { _manualOn = value; if ( !IsProxy ) UpdateEmitState(); }
	}
	private bool _manualOn;
	private bool _inputEmitting;

	private GameObject _particleInstance;
	private ScriptedEmitter _lastEmitter;

	protected override void OnStart() { }

	protected override void OnUpdate()
	{
		// Emitter resource changed — destroy existing instance so it gets recreated
		if ( _lastEmitter != Emitter && _particleInstance.IsValid() )
			DestroyParticle();

		_lastEmitter = Emitter;

		if ( IsEmitting && !_particleInstance.IsValid() )
			SpawnParticle();
		else if ( !IsEmitting && _particleInstance.IsValid() )
			DestroyParticle();
	}

	void IPlayerControllable.OnStartControl() { }
	void IPlayerControllable.OnEndControl()
	{
		if ( Mode == EmitMode.Hold )
		{
			_inputEmitting = false;
			UpdateEmitState();
		}
	}

	void IPlayerControllable.OnControl()
	{
		if ( Mode == EmitMode.Toggle )
		{
			if ( ToggleInput.Pressed() )
			{
				_inputEmitting = !_inputEmitting;
				UpdateEmitState();
			}
		}
		else
		{
			var held = HoldInput.Down();
			if ( held != _inputEmitting )
			{
				_inputEmitting = held;
				UpdateEmitState();
			}
		}
	}

	private void UpdateEmitState() => SetEmitting( _inputEmitting || _manualOn );

	[Rpc.Broadcast]
	private void SetEmitting( bool active )
	{
		IsEmitting = active;
	}

	private void SpawnParticle()
	{
		if ( !Emitter.IsValid() || Emitter.Prefab is null ) return;

		_particleInstance = GameObject.Clone( Emitter.Prefab, new CloneConfig
		{
			Parent = GameObject,
			Transform = new Transform( Vector3.Forward * 4f ),
			StartEnabled = true,
		} );
	}

	private void DestroyParticle()
	{
		_particleInstance.Destroy();
		_particleInstance = null;
	}
}
softsplit.sandbox / Game/Entity/SpotLightEntity.cs
Game game

public sealed class SpotLightEntity : Component, IPlayerControllable
{
	[Property, ClientEditable, Group( "Light" )]
	public bool On { get; set { field = value; UpdateLight(); } } = true;

	[Property, ClientEditable, Group( "Light" )]
	public bool Shadows { get; set { field = value; UpdateLight(); } } = true;

	[Property, Range( 0, 1 ), ClientEditable, Group( "Light" )]
	public Color Color { get; set { field = value; UpdateLight(); } }

	[Property, Range( 0, 50 ), ClientEditable, Group( "Light" )]
	public float Brightness { get; set { field = value; UpdateLight(); } } = 2;

	[Property, Range( 0, 1000 ), ClientEditable, Group( "Light" )]
	public float Radius { get; set { field = value; UpdateLight(); } } = 500;

	[Property, Range( 0, 90 ), ClientEditable, Group( "Light" )]
	public float Angle { get; set { field = value; UpdateLight(); } } = 35;

	[Property, Range( 0, 16 ), ClientEditable, Group( "Light" )]
	public float Attenuation { get; set { field = value; UpdateLight(); } } = 2.4f;


	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOn { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput TurnOff { get; set; }

	[Property, Sync, ClientEditable, Group( "State" )]
	public ClientInput Toggle { get; set; }

	[Property]
	public GameObject OnGameObject { get; set; }

	[Property]
	public GameObject OffGameObject { get; set; }

	void IPlayerControllable.OnControl()
	{

		if ( Toggle.Pressed() )
		{
			On = !On;
		}

		if ( TurnOn.Pressed() )
		{
			On = true;
		}

		if ( TurnOff.Pressed() )
		{
			On = false;
		}
	}

	void IPlayerControllable.OnEndControl()
	{

	}

	void IPlayerControllable.OnStartControl()
	{

	}

	void UpdateLight()
	{
		OnGameObject?.Enabled = On;
		OffGameObject?.Enabled = !On;

		if ( GetComponentInChildren<SpotLight>( true ) is not SpotLight light )
			return;

		light.Enabled = On;

		var color = Color;
		color.r *= Brightness;
		color.g *= Brightness;
		color.b *= Brightness;

		light.Shadows = Shadows;
		light.LightColor = color;
		light.Radius = Radius;
		light.Attenuation = Attenuation;
		light.ConeOuter = Angle;
		light.ConeInner = Angle * 0.5f;

		Network.Refresh();
	}
}
softsplit.sandbox / Game/Weapon/BaseBulletWeapon/BaseBulletWeapon.cs
Game game
public partial class BaseBulletWeapon : BaseWeapon
{
	[Property]
	public SoundEvent ShootSound { get; set; }

	[Property, Group( "Bullet" )]
	public BulletConfiguration Bullet { get; set; } = new()
	{
		Damage = 12f,
		BulletRadius = 1f,
		Range = 4096f,
		AimConeBase = new Vector2( 0.5f, 0.25f ),
		AimConeSpread = new Vector2( 3f, 3f ),
		AimConeRecovery = 0.2f,
		RecoilPitch = new Vector2( -0.3f, -0.1f ),
		RecoilYaw = new Vector2( -0.1f, 0.1f ),
		CameraRecoilStrength = 1f,
		CameraRecoilFrequency = 1f,
	};

	[Property, Group( "Bullet" ), ClientEditable, Range( 0f, 500000f ), Step( 10f )]
	public float ShootForce { get; set; } = 100000f;

	protected TimeSince TimeSinceShoot = 0;

	/// <summary>
	/// Returns 0 for no aim spread, 1 for full aim cone, based on time since last shot.
	/// </summary>
	protected float GetAimConeAmount( float recovery )
	{
		return TimeSinceShoot.Relative.Remap( 0, recovery, 1, 0 );
	}

	/// <summary>
	/// Returns the aim cone amount using the configured recovery time
	/// </summary>
	protected float GetAimConeAmount()
	{
		return GetAimConeAmount( Bullet.AimConeRecovery );
	}

	/// <inheritdoc cref="ShootBullet(float, in BulletConfiguration)"/>
	protected void ShootBullet( float fireRate )
	{
		ShootBullet( fireRate, Bullet );
	}

	/// <summary>
	/// Shoot a bullet out of the front of the gun.
	/// When held by a player, fires from the player's eye with aim cone and recoil.
	/// When standalone (no owner), fires straight from the weapon's muzzle.
	/// </summary>
	protected void ShootBullet( float fireRate, in BulletConfiguration config )
	{
		if ( HasOwner && ( !HasAmmo() || IsReloading() ) )
		{
			TryAutoReload();
			return;
		}

		if ( TimeUntilNextShotAllowed > 0 )
			return;

		// Only consume ammo when held by a player
		if ( HasOwner && !TakeAmmo( 1 ) )
		{
			AddShootDelay( 0.2f );
			return;
		}

		AddShootDelay( fireRate );

		var aimConeAmount = GetAimConeAmount( config.AimConeRecovery );
		var forward = AimRay.Forward
			.WithAimCone(
				config.AimConeBase.x + aimConeAmount * config.AimConeSpread.x,
				config.AimConeBase.y + aimConeAmount * config.AimConeSpread.y
			);
		var traceRay = AimRay with { Forward = forward };

		var tr = Scene.Trace.Ray( traceRay, config.Range )
			.IgnoreGameObjectHierarchy( AimIgnoreRoot )
			.WithCollisionRules( "bullet" )
			.WithoutTags( "playercontroller" )
			.Radius( config.BulletRadius )
			.UseHitboxes()
			.Run();

		ShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject, tr.Surface );
		TraceAttack( TraceAttackInfo.From( tr, config.Damage ) );
		TimeSinceShoot = 0;

		// Recoil only applies when held by a player
		if ( !HasOwner )
		{
			// Simulate physical recoil by pushing the weapon opposite to its fire direction
			if ( ShootForce > 0f && GetComponent<Rigidbody>( true ) is var rb )
			{
				var muzzle = WeaponModel?.MuzzleTransform?.WorldTransform ?? WorldTransform;
				rb.ApplyForce( muzzle.Rotation.Up * ShootForce );
			}
			return;
		}

		Owner.Controller.EyeAngles += new Angles(
			Random.Shared.Float( config.RecoilPitch.x, config.RecoilPitch.y ),
			Random.Shared.Float( config.RecoilYaw.x, config.RecoilYaw.y ),
			0
		);

		if ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )
		{
			_ = new Sandbox.CameraNoise.Recoil( config.CameraRecoilStrength, config.CameraRecoilFrequency );
		}
	}

	[Rpc.Broadcast]
	public void ShootEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject, Surface hitSurface, Vector3? origin = null, bool noEvents = false )
	{
		if ( Application.IsDedicatedServer ) return;
		if ( !hitSurface.IsValid() ) return;

		Owner?.Controller.Renderer.Set( "b_attack", true );

		if ( !noEvents )
		{
			if ( WeaponModel.IsValid() )
			{
				WeaponModel.GameObject.RunEvent<WeaponModel>( x => x.OnAttack() );
				WeaponModel.GameObject.RunEvent<WeaponModel>( x => x.CreateRangedEffects( this, hitpoint, origin ) );
			}

			if ( ShootSound.IsValid() )
			{
				var snd = GameObject.PlaySound( ShootSound );

				// If we're shooting, the sound should not be spatialized
				if ( HasOwner && Owner.IsLocalPlayer && snd.IsValid() )
				{
					snd.SpacialBlend = 0;
				}
			}
		}

		if ( !hit || !hitObject.IsValid() )
			return;

		var baseSurface = hitSurface.GetBaseSurface();
		var bulletSound = hitSurface.SoundCollection.Bullet ?? baseSurface?.SoundCollection.Bullet;
		if ( bulletSound.IsValid() )
		{
			Sound.Play( bulletSound, hitpoint );
		}

		var prefab = hitSurface.PrefabCollection.BulletImpact ?? baseSurface?.PrefabCollection.BulletImpact;

		// Still null?
		if ( prefab is null )
			return;

		var fwd = Rotation.LookAt( normal * -1.0f, Vector3.Random );

		var impact = prefab.Clone();
		impact.WorldPosition = hitpoint;
		impact.WorldRotation = fwd;
		impact.SetParent( hitObject, true );

		if ( hitObject.GetComponentInChildren<SkinnedModelRenderer>() is not { CreateBoneObjects: true } skinned )
			return;

		// find closest bone
		var bones = skinned.GetBoneTransforms( true );

		var closestDist = float.MaxValue;

		for ( var i = 0; i < bones.Length; i++ )
		{
			var bone = bones[i];
			var dist = bone.Position.Distance( hitpoint );
			if ( dist < closestDist )
			{
				closestDist = dist;
				impact.SetParent( skinned.GetBoneObject( i ), true );
			}
		}
	}

	public record struct BulletConfiguration
	{
		public float Damage { get; set; }
		public float BulletRadius { get; set; }
		public Vector2 AimConeBase { get; set; }
		public Vector2 AimConeSpread { get; set; }
		public float AimConeRecovery { get; set; }
		public Vector2 RecoilPitch { get; set; }
		public Vector2 RecoilYaw { get; set; }
		public float CameraRecoilStrength { get; set; }
		public float CameraRecoilFrequency { get; set; }
		public float Range { get; set; }
	}
}
softsplit.sandbox / Game/Weapon/BaseWeapon/BaseWeapon.Reloading.cs
Game game
using System.Threading;

public partial class BaseWeapon
{
	/// <summary>
	/// Should we consume 1 bullet per reload instead of filling the clip?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool IncrementalReloading { get; set; } = false;

	/// <summary>
	/// Extra delay after the first shell reload before subsequent shells begin (e.g. longer carrier insertion animation).
	/// Only used with incremental reloading. If zero, no extra delay is added.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float FirstShellReloadTime { get; set; } = 0f;

	/// <summary>
	/// Delay before the first shell is inserted during incremental reload.
	/// If zero, uses <see cref="ReloadTime"/>.
	/// </summary>
	[Property, Feature( "Ammo" ), ShowIf( nameof( IncrementalReloading ), true )]
	public float ReloadStartTime { get; set; } = 0f;

	/// <summary>
	/// Can we cancel reloads?
	/// </summary>
	[Property, Feature( "Ammo" )]
	public bool CanCancelReload { get; set; } = true;

	private CancellationTokenSource reloadToken;
	private bool isReloading;

	public bool CanReload()
	{
		if ( !UsesClips ) return false;
		if ( ClipContents >= ClipMaxSize ) return false;
		if ( isReloading ) return false;
		if ( !WeaponConVars.InfiniteReserves && ReserveAmmo <= 0 ) return false;

		return true;
	}

	public bool IsReloading() => isReloading;

	public virtual void CancelReload()
	{
		if ( reloadToken?.IsCancellationRequested == false )
		{
			reloadToken?.Cancel();
			isReloading = false;

			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadCancel() );
		}
	}

	public virtual async void OnReloadStart()
	{
		if ( !CanReload() )
			return;

		CancelReload();

		var cts = new CancellationTokenSource();
		reloadToken = cts;
		isReloading = true;

		try
		{
			await ReloadAsync( cts.Token );
		}
		finally
		{
			// Only clean up our own reload
			if ( reloadToken == cts )
			{
				isReloading = false;
				reloadToken = null;
			}
			cts.Dispose();
		}
	}

	[Rpc.Broadcast]
	private void BroadcastReload()
	{
		if ( !HasOwner ) return;

		Assert.True( Owner.Controller.IsValid(), "BaseWeapon::BroadcastReload - Player Controller is invalid!" );
		Assert.True( Owner.Controller.Renderer.IsValid(), "BaseWeapon::BroadcastReload - Renderer is invalid!" );

		Owner.Controller.Renderer.Set( "b_reload", true );
	}

	protected virtual async Task ReloadAsync( CancellationToken ct )
	{
		// Capture so we can tell if a newer reload has replaced us by the time finally runs.
		var mySource = reloadToken;
		var isFirstShell = ClipContents == 0;

		try
		{
			ViewModel?.RunEvent<ViewModel>( x => x.OnReloadStart() );

			BroadcastReload();

			var firstIteration = true;

			while ( ClipContents < ClipMaxSize && !ct.IsCancellationRequested )
				{
					var delay = (firstIteration && IncrementalReloading && ReloadStartTime > 0f) ? ReloadStartTime : ReloadTime;
					firstIteration = false;
					await Task.DelaySeconds( delay, ct );

					var needed = IncrementalReloading ? 1 : (ClipMaxSize - ClipContents);

					if ( WeaponConVars.InfiniteReserves )
					{
						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );
						ClipContents += needed;
					}
					else
					{
						var available = Math.Min( needed, ReserveAmmo );

						if ( available <= 0 )
							break;

						ViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );

						ReserveAmmo -= available;
						ClipContents += available;
					}

					// After the first shell, wait longer before the next one starts
					if ( isFirstShell && FirstShellReloadTime > 0f )
					{
						await Task.DelaySeconds( FirstShellReloadTime, ct );
					}

					isFirstShell = false;
				}
		}
		finally
		{
			if ( reloadToken == mySource )
			{
				ViewModel?.RunEvent<ViewModel>( x => x.OnReloadFinish() );
			}
		}
	}
}
softsplit.sandbox / GameLoop/GamePreferences.cs
Game game
/// <summary>
/// The local user's preferences in Deathmatch
/// </summary>
internal static class GamePreferences
{
	/// <summary>
	/// Enables automatic switching to better weapons on item pickup
	/// </summary>
	[ConVar( "sb.autoswitch", ConVarFlags.UserInfo | ConVarFlags.Saved )]
	public static bool AutoSwitch { get; set; } = true;

	/// <summary>
	/// Enables fast switching between inventory weapons
	/// </summary>
	[ConVar( "sb.fastswitch", ConVarFlags.Saved )]
	public static bool FastSwitch { get; set; } = false;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.viewbob", ConVarFlags.Saved )]
	[Group( "Camera" )]
	public static bool ViewBobbing { get; set; } = true;

	/// <summary>
	/// Intensity of your camera's screenshake
	/// </summary>
	[ConVar( "sb.screenshake", ConVarFlags.Saved )]
	[Range( 0.1f, 2f ), Step( 0.1f ), Group( "Camera" )]
	public static float Screenshake { get; set; } = 0.3f;
}
softsplit.sandbox / Npcs/NpcConVars.cs
Game game
namespace Sandbox.Npcs;

/// <summary>
/// Console variables that control NPC AI behaviour globally.
/// </summary>
public static class NpcConVars
{
	/// <summary>
	/// When disabled, all NPC AI thinking is paused — they just stand idle.
	/// </summary>
	[ConVar( "sb.ai.enabled", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "Enable or disable NPC AI thinking." )]
	public static bool Enabled { get; set; } = true;

	/// <summary>
	/// When enabled, NPCs cannot target players.
	/// </summary>
	[ConVar( "sb.ai.notarget", ConVarFlags.Replicated | ConVarFlags.Saved, Help = "When enabled, NPCs cannot target players." )]
	public static bool NoTarget { get; set; } = false;
}
softsplit.sandbox / Npcs/Scientist/ScientistFleeSchedule.cs
Game game
using Sandbox.Npcs.Layers;
using Sandbox.Npcs.Tasks;

namespace Sandbox.Npcs.Schedules;

/// <summary>
/// Panic flee — scream while sprinting away from the source.
/// </summary>
public sealed class ScientistFleeSchedule : ScheduleBase
{
	private static readonly string[] PanicLines =
	[
		"AHHH!",
		"Don't hurt me!",
		"Help! HELP!",
		"Stay away from me!",
		"I'm just a scientist!",
		"Please, no!",
		"Somebody help!",
		"Oh god oh god oh god!",
		"What did I do?!",
		"Leave me alone!",
	];

	public GameObject Source { get; set; }

	/// <summary>
	/// 0–1 panic intensity. Higher values mean faster speed and longer flee distance.
	/// </summary>
	public float PanicLevel { get; set; } = 0.5f;

	protected override void OnStart()
	{
		if ( !Source.IsValid() ) return;

		// Sprint speed scales with panic (200–350)
		Npc.Navigation.WishSpeed = 200f + 150f * PanicLevel;

		// Don't stare at the player — look where we're running
		Npc.Animation.ClearLookTarget();

		// Scream immediately — but only if not already mid-speech
		if ( Npc.Speech.CanSpeak )
		{
			var line = PanicLines[Game.Random.Int( 0, PanicLines.Length - 1 )];
			Npc.Speech.Say( line, 2f );
		}

		// Flee direction — away from the attacker with some randomness
		var awayDir = (GameObject.WorldPosition - Source.WorldPosition).WithZ( 0 ).Normal;
		var randomAngle = Game.Random.Float( -40f, 40f );
		awayDir = Rotation.FromAxis( Vector3.Up, randomAngle ) * awayDir;

		// Distance scales with panic (200–500)
		var fleeDist = 512f + 1024f * PanicLevel;
		var fleeTarget = GameObject.WorldPosition + awayDir * fleeDist;

		// Snap to navmesh
		if ( Npc.Scene.NavMesh.GetClosestPoint( fleeTarget ) is { } navPoint )
		{
			AddTask( new MoveTo( navPoint, 15f ) );
		}
		else
		{
			AddTask( new MoveTo( fleeTarget, 15f ) );
		}
	}

	protected override void OnEnd()
	{
		// Reset to normal walk speed
		// TODO: this is shit, can we scope these somehow so the IDisposable handles all this ?
		Npc.Navigation.WishSpeed = 100f;
	}

	protected override bool ShouldCancel()
	{
		return !Source.IsValid();
	}
}
softsplit.sandbox / Player/PlayerFallDamage.cs
Game game
/// <summary>
/// Apply fall damage to the player
/// </summary>
public class PlayerFallDamage : Component, Local.IPlayerEvents
{
	[RequireComponent] public Player Player { get; set; }

	/// <summary>
	/// Fatal fall speed, you will die if you fall at or above this speed
	/// </summary>
	[Property] public float FatalFallSpeed { get; set; } = 1536.0f;

	/// <summary>
	/// Maximum safe fall speed, you won't take damage at or below this speed
	/// </summary>
	[Property] public float MaxSafeFallSpeed { get; set; } = 512.0f;

	/// <summary>
	/// Multiply damage amount by this much
	/// </summary>
	[Property] public float DamageMultiplier { get; set; } = 1.0f;

	/// <summary>
	/// Fall damage sound
	/// </summary>
	[Property] public SoundEvent FallSound { get; set; }

	[Rpc.Owner]
	private void PlayFallSound()
	{
		GameObject.PlaySound( FallSound );
	}

	void Local.IPlayerEvents.OnLand( float distance, Vector3 velocity )
	{
		var fallSpeed = Math.Abs( velocity.z );

		if ( fallSpeed <= MaxSafeFallSpeed )
			return;

		var damageAmount = MathX.Remap( fallSpeed, MaxSafeFallSpeed, FatalFallSpeed, 0f, 100f ) * DamageMultiplier;
		if ( damageAmount < 1 ) return;

		if ( Networking.IsHost && damageAmount >= Player.Health )
			Player.PlayerData?.AddStat( "player.fall.death" );

		TakeFallDamage( damageAmount );
	}


	[Rpc.Broadcast]
	public void TakeFallDamage( float amount )
	{
		if ( !Networking.IsHost ) return;


		if ( Player is IDamageable damage )
		{
			var dmg = new DamageInfo( amount.CeilToInt(), Player.GameObject, null );
			dmg.Tags.Add( DamageTags.Fall );
			damage.OnDamage( dmg );

			PlayFallSound();
		}
	}
}
softsplit.sandbox / Player/PlayerLoadout.cs
Game game
/// <summary>
/// Manages loadout persistence, presets, and restoration for a player.
/// Lives on the Player GameObject alongside PlayerInventory.
/// Listens to inventory events to auto-save, and handles all loadout RPCs directly.
/// </summary>
public sealed class PlayerLoadout : Component, Local.IPlayerEvents, Global.IPlayerEvents, Global.ISaveEvents
{
	[RequireComponent] public Player Player { get; set; }
	[RequireComponent] public PlayerInventory Inventory { get; set; }

	private bool _isRestoringLoadout;

	/// <summary>
	/// One entry in a serialized loadout: the prefab resource path and the slot it occupies.
	/// </summary>
	public struct LoadoutEntry
	{
		public string PrefabPath { get; set; }
		public int Slot { get; set; }
		public string SpawnerDataPayload { get; set; }
	}

	public struct SavedPreset
	{
		public string Name { get; set; }
		public string LoadoutJson { get; set; }
	}

	public static IReadOnlyList<SavedPreset> GetLoadoutPresets()
	{
		return LocalData.Get<List<SavedPreset>>( "presets", new() );
	}

	public static void SaveLoadoutPreset( string name, string loadoutJson )
	{
		var presets = LocalData.Get<List<SavedPreset>>( "presets", new() );
		var idx = presets.FindIndex( p => p.Name == name );
		var entry = new SavedPreset { Name = name, LoadoutJson = loadoutJson };
		if ( idx >= 0 )
			presets[idx] = entry;
		else
			presets.Add( entry );
		LocalData.Set( "presets", presets );
	}

	public static void DeleteLoadoutPreset( string name )
	{
		var presets = LocalData.Get<List<SavedPreset>>( "presets", new() );
		presets.RemoveAll( p => p.Name == name );
		LocalData.Set( "presets", presets );
	}

	public string SerializeLoadout()
	{
		var entries = Inventory.Weapons
			.Where( w => !string.IsNullOrEmpty( w.GameObject.PrefabInstanceSource ) )
			.Select( w => new LoadoutEntry
			{
				PrefabPath = w.GameObject.PrefabInstanceSource,
				Slot = w.InventorySlot,
				SpawnerDataPayload = (w as SpawnerWeapon)?.SpawnerData
			} )
			.ToList();

		return entries.Count > 0 ? Json.Serialize( entries ) : null;
	}

	public void SaveLoadout()
	{
		if ( _isRestoringLoadout ) return;

		var json = SerializeLoadout();
		if ( string.IsNullOrEmpty( json ) ) return;

		if ( Player.IsLocalPlayer )
		{
			LocalData.Set( "hotbar", json );
		}
		else
		{
			PushLoadoutToClient( json );
		}
	}

	public void GiveLoadoutWeapons( string json )
	{
		var entries = Json.Deserialize<List<LoadoutEntry>>( json );
		if ( entries is null ) return;

		_isRestoringLoadout = true;
		try
		{
			foreach ( var entry in entries )
			{
				if ( !Inventory.Pickup( entry.PrefabPath, entry.Slot, false ) )
					continue;

				if ( !string.IsNullOrEmpty( entry.SpawnerDataPayload ) && Inventory.GetSlot( entry.Slot ) is SpawnerWeapon spawnerWeapon )
				{
					spawnerWeapon.RestoreSpawnerData( entry.SpawnerDataPayload );
				}
			}
		}
		finally
		{
			_isRestoringLoadout = false;
		}
	}

	private static async Task EnsureMountedAsync( string json )
	{
		var entries = Json.Deserialize<List<LoadoutEntry>>( json );
		if ( entries is null ) return;

		var needsMounts = entries.Any( e => !string.IsNullOrEmpty( e.SpawnerDataPayload )
			&& e.SpawnerDataPayload.EndsWith( ".vmdl", StringComparison.OrdinalIgnoreCase ) );

		if ( !needsMounts ) return;

		foreach ( var entry in Sandbox.Mounting.Directory.GetAll().Where( e => e.Available ) )
			await Sandbox.Mounting.Directory.Mount( entry.Ident );
	}

	public void SwitchToPreset( string loadoutJson )
	{
		if ( !Networking.IsHost )
		{
			HostSwitchToPreset( loadoutJson );
			return;
		}
		_ = SwitchToPresetAsync( loadoutJson );
	}

	public void ResetToDefault()
	{
		if ( !Networking.IsHost )
		{
			HostResetToDefault();
			return;
		}
		_ = ResetToDefaultAsync();
	}

	[Rpc.Host]
	private void HostSwitchToPreset( string loadoutJson )
	{
		_ = SwitchToPresetAsync( loadoutJson );
	}

	[Rpc.Host]
	private void HostResetToDefault()
	{
		_ = ResetToDefaultAsync();
	}

	private async Task SwitchToPresetAsync( string loadoutJson )
	{
		var previousSlot = Inventory.ActiveWeapon?.InventorySlot ?? 0;

		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		await EnsureMountedAsync( loadoutJson );
		GiveLoadoutWeapons( loadoutJson );

		var toEquip = Inventory.GetSlot( previousSlot ) ?? Inventory.GetBestWeapon();
		if ( toEquip.IsValid() )
			Inventory.SwitchWeapon( toEquip );

		SaveLoadout();
	}

	private async Task ResetToDefaultAsync()
	{
		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		Inventory.GiveDefaultWeapons();
		Inventory.SwitchWeapon( Inventory.GetBestWeapon() );
		SaveLoadout();
	}

	[Rpc.Owner]
	private void PushLoadoutToClient( string loadoutJson )
	{
		LocalData.Set( "hotbar", loadoutJson );
	}

	[Rpc.Owner]
	private void RequestClientLoadout()
	{
		var json = LocalData.Get<string>( "hotbar" );
		if ( !string.IsNullOrEmpty( json ) )
			HostRestoreLoadoutFromClient( json );
	}

	/// <summary>
	/// Clears the current inventory, waits a frame, then gives the loadout from JSON and equips the best weapon.
	/// </summary>
	private async Task ReplaceLoadoutAsync( string json )
	{
		foreach ( var weapon in Inventory.Weapons.ToList() )
			weapon.DestroyGameObject();

		await Task.Yield();

		await EnsureMountedAsync( json );
		GiveLoadoutWeapons( json );

		var best = Inventory.GetBestWeapon();
		if ( best.IsValid() )
			Inventory.SwitchWeapon( best );
	}

	[Rpc.Host]
	private async void HostRestoreLoadoutFromClient( string loadoutJson )
	{
		await ReplaceLoadoutAsync( loadoutJson );
	}

	void Global.IPlayerEvents.OnPlayerSpawned( Player player )
	{
		if ( player != Player ) return;
		if ( !Networking.IsHost ) return;

		_ = RestoreOnSpawnAsync();
	}

	private async Task RestoreOnSpawnAsync()
	{
		if ( Player.IsLocalPlayer )
		{
			var json = LocalData.Get<string>( "hotbar" );
			if ( !string.IsNullOrEmpty( json ) )
			{
				await ReplaceLoadoutAsync( json );
				return;
			}
		}
		else
		{
			RequestClientLoadout();
			return;
		}

		Inventory.GiveDefaultWeapons();
		var bestWeapon = Inventory.GetBestWeapon();
		if ( bestWeapon.IsValid() )
			Inventory.SwitchWeapon( bestWeapon );
	}

	void Local.IPlayerEvents.OnDied( PlayerDiedParams args )
	{
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	void Local.IPlayerEvents.OnPickup( PlayerPickupEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	void Local.IPlayerEvents.OnDrop( PlayerDropEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		_ = SaveLoadoutAfterYield();
	}

	void Local.IPlayerEvents.OnRemoveWeapon( PlayerRemoveWeaponEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		_ = SaveLoadoutAfterYield();
	}

	void Local.IPlayerEvents.OnMoveSlot( PlayerMoveSlotEvent e )
	{
		if ( e.Cancelled ) return;
		if ( !Networking.IsHost ) return;
		SaveLoadout();
	}

	private async Task SaveLoadoutAfterYield()
	{
		await Task.Yield();
		SaveLoadout();
	}

	void Global.ISaveEvents.BeforeSave( string filename )
	{
		if ( !Networking.IsHost ) return;

		var steamId = (long)(Player.Network.Owner?.SteamId ?? 0);
		if ( steamId == 0 ) return;

		var json = SerializeLoadout();
		if ( string.IsNullOrEmpty( json ) ) return;

		SaveSystem.Current?.SetMetadata( $"Loadout_{steamId}", json );
	}

	void Global.ISaveEvents.AfterLoad( string filename )
	{
		if ( !Networking.IsHost ) return;

		var steamId = (long)(Player.Network.Owner?.SteamId ?? 0);
		if ( steamId == 0 ) return;

		var json = SaveSystem.Current?.GetMetadata( $"Loadout_{steamId}" );
		if ( string.IsNullOrEmpty( json ) ) return;

		_ = RestoreLoadoutFromSaveAsync( json );
	}

	private async Task RestoreLoadoutFromSaveAsync( string json )
	{
		await ReplaceLoadoutAsync( json );
	}
}
softsplit.sandbox / Player/PlayerObserver.cs
Game game
/// <summary>
/// Dead players become these. They try to observe their last corpse. 
/// </summary>
internal sealed class PlayerObserver : Component
{
	Angles EyeAngles;
	TimeSince timeSinceStarted;
	DeathCameraTarget _cachedCorpse;
	float currentDistance;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		EyeAngles = Scene.Camera.WorldRotation;
		timeSinceStarted = 0;
		currentDistance = 32;

		_cachedCorpse = Scene.GetAllComponents<DeathCameraTarget>()
					.Where( x => x.Connection == Network.Owner )
					.OrderByDescending( x => x.Created )
					.FirstOrDefault();
	}

	protected override void OnUpdate()
	{
		// Don't allow immediate respawn
		if ( timeSinceStarted < 1 )
			return;

		// If pressed a button, or has been too long
		if ( Input.Pressed( "attack1" ) || Input.Pressed( "jump" ) || timeSinceStarted > 4f )
		{
			GameManager.Current?.RequestRespawn();
			GameObject.Destroy();
		}
	}

	protected override void OnPreRender()
	{
		if ( IsProxy ) return;

		if ( _cachedCorpse.IsValid() )
		{
			RotateAround( _cachedCorpse );
		}
	}

	private void RotateAround( Component target )
	{
		// Find the corpse eyes
		if ( target.Components.Get<SkinnedModelRenderer>().TryGetBoneTransform( "pelvis", out var tx ) )
		{
			tx.Position += Vector3.Up * 25;
		}

		var e = EyeAngles;
		e += Input.AnalogLook;
		e.pitch = e.pitch.Clamp( -90, 90 );
		e.roll = 0.0f;
		EyeAngles = e;

		currentDistance = currentDistance.LerpTo( 150, Time.Delta * 5 );

		var center = tx.Position;
		var targetPos = center - EyeAngles.Forward * currentDistance;

		var tr = Scene.Trace.FromTo( center, targetPos ).Radius( 1.0f ).WithoutTags( "ragdoll", "effect" ).Run();

		Scene.Camera.WorldPosition = tr.EndPosition;
		Scene.Camera.WorldRotation = EyeAngles;
	}
}
softsplit.sandbox / UI/Components/SpawnMenuContent.razor
Game game
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox

<root>
    @if (Header != null)
    {
        <div class="header">@Header</div>
    }

    <div class="body menuinner">@Body</div>
</root>

@code
{

    [Parameter] public RenderFragment Header { get; set; }
    [Parameter] public RenderFragment Body { get; set; }

}
Debug: View Raw JSON Response
{
    "TotalCount": 420,
    "Files": [
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/controls/color/colorcontrol.cs.scss",
            "FileName": "colorcontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "ColorControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground-color: #000a;\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 32px;\r\n}\r\n\r\nColorControl TextEntry\r\n{\r\n\tflex-grow: 1;\r\n\tflex-shrink: 0;\r\n\tcolor: #aaa;\r\n\tfont-size: 1.2rem;\r\n\r\n\t&:hover, &:focus\r\n\t{\r\n\t\tcolor: #ddd;\r\n\r\n\t\t.icon\r\n\t\t{\r\n\t\t\tcolor: #3af;\r\n\t\t}\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tcolor: white;\r\n\t}\r\n}\r\n\r\nColorControl > .colorswatch\r\n{\r\n\taspect-ratio: 1;\r\n\tborder-radius: 4px;\r\n\theight: 100%;\r\n\tflex-shrink: 0;\r\n\tcursor: pointer;\r\n\tborder: 2px solid #000;\r\n}"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/contextmenu/componenthandle.cs.scss",
            "FileName": "componenthandle.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "ComponentHandle\r\n{\r\n\twidth: 38px;\r\n\theight: 38px;\r\n\tbackground-color: #eee;\r\n\tbox-shadow: 2px 2px 16px black;\r\n\tborder-radius: 20px;\r\n\tposition: absolute;\r\n\tjustify-content: center;\r\n\talign-items: center;\r\n\tcursor: pointer;\r\n\ttransform: scale( 1 ) translateX( -50% ) translateY( -50% );\r\n\tz-index: 100;\r\n\r\n\t.icon\r\n\t{\r\n\t\tfont-family: \"Material Icons\";\r\n\t\tcolor: #08f;\r\n\t\tfont-size: 23px;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t&:hover\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-color: #fff;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t\tbackground-color: #fff;\r\n\t\topacity: 1;\r\n\t\ttransform: scale( 0.95 ) translateX( -50% ) translateY( -50% );\r\n\t}\r\n\r\n\t&.active\r\n\t{\r\n\t\tborder: 2px solid #fff;\r\n\t\tbackground-color: #08f;\r\n\t\topacity: 1;\r\n\r\n\t\t.icon\r\n\t\t{\r\n\t\t\tcolor: #fff;\r\n\t\t}\r\n\t}\r\n\r\n\t&:outro\r\n\t{\r\n\t\ttransform: scale( 0 ) translateX( -50% ) translateY( -50% );\r\n\t\ttransition: transform 0.2s ease-out;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/controlsheet/controlsheetrow.cs.scss",
            "FileName": "controlsheetrow.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "\r\nControlSheetRow\r\n{\r\n\tflex-direction: row;\r\n\tflex-shrink: 0;\r\n\tmax-height: 32px;\r\n\tborder-radius: 4px;\r\n\r\n\t&.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n\r\nControlSheetRow > .left\r\n{\r\n\twidth: 150px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\tpadding: 6px;\r\n}\r\n\r\nControlSheetRow > .right\r\n{\r\n\tflex-grow: 1;\r\n}\r\n\r\nControlSheetRow > .left > .title\r\n{\r\n\twidth: 150px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\tfont-size: 13px;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n}\r\n\r\nControlSheetRow > .right > textentry,\r\nControlSheetRow > .right numberentry\r\n{\r\n\tbackground-color: #000a;\r\n\tflex-grow: 1;\r\n\tpadding: 4px 8px;\r\n\tborder-radius: 2px;\r\n}\r\n\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/pressable/pressablehud.razor.scss",
            "FileName": "pressablehud.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "@import \"/UI/Theme.scss\";\r\n\r\nPressableHud\r\n{\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tleft: 0;\r\n\tright: 0;\r\n\tbottom: 0;\r\n\tjustify-content: center;\r\n\talign-items: center;\r\n\tfont-family: $body-font;\r\n\tfont-size: 14px;\r\n\tfont-weight: 600;\r\n\tz-index: 1000;\r\n\topacity: 0.2;\r\n\r\n\t.crosshair\r\n\t{\r\n\t\twidth: 12px;\r\n\t\theight: 12px;\r\n\t\tbackground-color: white;\r\n\t\tborder-radius: 50%;\r\n\t\tbox-shadow: 0px 0px 8px black;\r\n\t\topacity: 0;\r\n\t\ttransition: all 0.1s linear;\r\n\t}\r\n\r\n\t&.active\r\n\t{\r\n\t\topacity: 1;\r\n\r\n\t\t.crosshair\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/theme.scss",
            "FileName": "theme.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "$deadzone-x: 2vw;\r\n$deadzone-y: 2vw;\r\n\r\n$color-accent: #0094ff;\r\n$color-text: #fafaff;\r\n\r\n$title-font: \"Poppins\";\r\n$body-font: \"Inter\";\r\n$subtitle-font: \"Poppins\";\r\n\r\n@import \"/UI/Menu.scss\";\r\n@import \"/UI/Hud.scss\";\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/faceposer/faceposeeditor.razor.scss",
            "FileName": "faceposeeditor.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "@import \"/UI/Theme.scss\";\r\n\r\nFacePoseEditor\r\n{\r\n    flex-direction: column;\r\n    width: 100%;\r\n    max-height: 400px;\r\n}\r\n\r\nFacePoseEditor .group-header\r\n{\r\n    font-size: 11px;\r\n    font-weight: 700;\r\n    color: rgba( 255, 255, 255, 0.35 );\r\n    padding: 6px 6px 2px 6px;\r\n    text-transform: uppercase;\r\n    letter-spacing: 0.08em;\r\n    flex-shrink: 0;\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "ui/menu.scss",
            "FileName": "menu.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "$menu-color:#cdf;\r\n$menu-text: #fffa;\r\n$menu-text-strong: #fff;\r\n$menu-surface-soft: #0004;\r\n$menu-accent: $color-accent;\r\n$menu-accent-soft: rgba($color-accent, 0.13);\r\n$menu-panel-bg: #0c202fcc;\r\n\r\n.menu-header-bar\r\n{\r\n\tborder-radius: 8px 8px 0 0;\r\n\tz-index: 100;\r\n\tfont-family: $title-font;\r\n\tfont-weight: 600;\r\n}\r\n\r\n.menu-input\r\n{\r\n\tbackground-color: $menu-surface-soft;\r\n\tborder-radius: 8px;\r\n\tpadding: 2px 10px;\r\n}\r\n\r\n.menu-segmented-group\r\n{\r\n\tbackground-color: #0008;\r\n\tborder-radius: 8px;\r\n\toverflow: visible;\r\n}\r\n\r\n.menu-mode-button\r\n{\r\n\tcolor: $menu-text;\r\n\tfont-size: 1rem;\r\n\tfont-weight: 600;\r\n\tpadding: 1rem;\r\n\tfont-family: $subtitle-font;\r\n\tcursor: pointer;\r\n\tpointer-events: all;\r\n\tjustify-content: center;\r\n\talign-items: center;\r\n\toverflow: visible;\r\n\tborder-radius: 8px;\r\n\r\n\t.icon\r\n\t{\r\n\t\tmargin-left: -4px;\r\n\t\tmargin-top: -1.5px;\r\n\t\tpadding-right: 4px;\r\n\t\tfont-size: 1.5rem;\r\n\t}\r\n\r\n\t.title\r\n\t{\r\n\t\tfont-size: 1.1rem;\r\n\t\tfont-weight: 700;\r\n\t}\r\n\r\n\t&:hover\r\n\t{\r\n\t\tcolor: $menu-text-strong;\r\n\t}\r\n\r\n\t&.active\r\n\t{\r\n\t\toutline: 1px solid $menu-accent;\r\n\t\tbackground-color: rgba( $menu-accent, 0.2 );\r\n\t\tcolor: $menu-text-strong;\r\n\t}\r\n}\r\n\r\n.menu-icon-toggle-group\r\n{\r\n\tbackground-color: $menu-surface-soft;\r\n\tborder-radius: 8px;\r\n\tgap: 0.5rem;\r\n\tpadding: 4px 4px;\r\n\tpadding-left: 8px;\r\n\talign-items: center;\r\n\r\n\tIconPanel\r\n\t{\r\n\t\twidth: 20px;\r\n\t\taspect-ratio: 1;\r\n\t\topacity: 0.35;\r\n\t\talign-items: center;\r\n\t\tcursor: pointer;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\topacity: 0.7;\r\n\t\t}\r\n\r\n\t\t&.active\r\n\t\t{\r\n\t\t\topacity: 1;\r\n\t\t}\r\n\t}\r\n}\r\n\r\nspan\r\n{\r\n\t&.grow\r\n\t{\r\n\t\tgap: 8px;\r\n\r\n\t\t> *\r\n\t\t{\r\n\t\t\tflex-grow: 1;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n.menu-action\r\n{\r\n\tcolor: $menu-text-strong;\r\n\tborder: 1px solid transparent;\r\n\tborder-radius: 4px;\r\n\tcursor: pointer;\r\n\tpadding: 6px 10px;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\tgap: 0.5rem;\r\n\tfont-weight: 600;\r\n\r\n\tlabel, .title\r\n\t{\r\n\t\tfont-size: 14px;\r\n\t}\r\n\r\n\tIconPanel, .icon\r\n\t{\r\n\t\tfont-size: 1.2rem;\r\n\t}\r\n\r\n\t&.disabled\r\n\t{\r\n\t\tpointer-events: none;\r\n\t\topacity: 0.25;\r\n\t}\r\n\r\n\t&.soft\r\n\t{\r\n\t\tbackground-color: $menu-accent-soft;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tbackground-color: rgba($color-accent, 0.4);\r\n\t\t\tsound-in: ui.button.over;\r\n\t\t}\r\n\t}\r\n\r\n\t&.primary\r\n\t{\r\n\t\tborder-color: $menu-accent;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tbackground-color: $menu-accent-soft;\r\n\t\t\tsound-in: ui.button.over;\r\n\t\t}\r\n\t}\r\n\r\n\t&.wide\r\n\t{\r\n\t\twidth: 100%;\r\n\t}\r\n}\r\n\r\n.menu-filter-input\r\n{\r\n\twidth: 200px;\r\n}\r\n\r\nTextEntry \r\n{\r\n\tcolor: $color-text;\r\n\tfont-family: $body-font;\r\n}"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Cleanup/CleanupSystem.cs",
            "FileName": "CleanupSystem.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "using Sandbox.UI;\r\n\r\nnamespace Sandbox;\r\n\r\npublic interface ICleanupEvents\r\n{\r\n\tpublic void OnCleanup( int removedObjects, int restoredObjects );\r\n}\r\n\r\n/// <summary>\r\n/// A system that tracks the baseline scene state and allows resetting the map to its original state.\r\n/// Removes all spawned props and restores destroyed map objects while leaving players untouched.\r\n/// </summary>\r\ninternal sealed class CleanupSystem : GameObjectSystem<CleanupSystem>, ISceneLoadingEvents\r\n{\r\n\t/// <summary>\r\n\t/// Set of GameObjects that existed in the original scene baseline.\r\n\t/// </summary>\r\n\tprivate readonly HashSet<Guid> _baselineObjectIds = new();\r\n\r\n\t/// <summary>\r\n\t/// Serialized data of baseline objects so we can restore them if destroyed.\r\n\t/// </summary>\r\n\tprivate readonly Dictionary<Guid, string> _baselineObjectData = new();\r\n\r\n\tprivate static bool _restorePersistedBaseline;\r\n\tprivate static HashSet<Guid> _persistedBaselineIds;\r\n\tprivate static Dictionary<Guid, string> _persistedBaselineData;\r\n\r\n\t/// <summary>\r\n\t/// Whether a baseline has been captured.\r\n\t/// </summary>\r\n\tpublic bool HasBaseline => _baselineObjectIds.Count > 0;\r\n\r\n\tpublic CleanupSystem( Scene scene ) : base( scene )\r\n\t{\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Call from SaveSystem before Game.ChangeScene() to snapshot the current baseline\r\n\t/// </summary>\r\n\tpublic static void PreserveBaselineForSaveLoad()\r\n\t{\r\n\t\tif ( Current is null || !Current.HasBaseline ) return;\r\n\r\n\t\t_restorePersistedBaseline = true;\r\n\t\t_persistedBaselineIds = new HashSet<Guid>( Current._baselineObjectIds );\r\n\t\t_persistedBaselineData = new Dictionary<Guid, string>( Current._baselineObjectData );\r\n\t}\r\n\r\n\tvoid ISceneLoadingEvents.BeforeLoad( Scene scene, SceneLoadOptions options )\r\n\t{\r\n\t\t// Clear any existing baseline when a new scene is loading\r\n\t\t_baselineObjectIds.Clear();\r\n\t\t_baselineObjectData.Clear();\r\n\t}\r\n\r\n\tasync Task ISceneLoadingEvents.OnLoad( Scene scene, SceneLoadOptions options, LoadingContext context )\r\n\t{\r\n\t\t// We don't care if the game is not playing\r\n\t\tif ( !Game.IsPlaying ) return;\r\n\r\n\t\t// Wait for next frame to ensure all objects are spawned\r\n\t\tawait Task.Yield();\r\n\r\n\t\t// Could be null if the scene was unloaded before this runs\r\n\t\tif ( !Scene.IsValid() ) return;\r\n\r\n\t\t// When loading a save, restore the baseline captured before the scene was destroyed\r\n\t\tif ( _restorePersistedBaseline && _persistedBaselineIds is not null )\r\n\t\t{\r\n\t\t\t_baselineObjectIds.UnionWith( _persistedBaselineIds );\r\n\t\t\tforeach ( var kvp in _persistedBaselineData )\r\n\t\t\t\t_baselineObjectData.TryAdd( kvp.Key, kvp.Value );\r\n\r\n\t\t\t_restorePersistedBaseline = false;\r\n\t\t\tLog.Info( $\"CleanupSystem: Restored persisted baseline with {_baselineObjectIds.Count} objects.\" );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tCaptureBaseline();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Captures the current scene state as the baseline.\r\n\t/// All objects that exist at this point are considered part of the original map.\r\n\t/// </summary>\r\n\tpublic void CaptureBaseline()\r\n\t{\r\n\t\t_baselineObjectIds.Clear();\r\n\t\t_baselineObjectData.Clear();\r\n\r\n\t\tforeach ( var go in Scene.Children?.ToArray() ?? [] )\r\n\t\t{\r\n\t\t\tCaptureObjectRecursive( go );\r\n\t\t}\r\n\r\n\t\tLog.Info( $\"CleanupSystem: Captured baseline with {_baselineObjectIds.Count} objects.\" );\r\n\t}\r\n\r\n\tprivate void CaptureObjectRecursive( GameObject go )\r\n\t{\r\n\t\tif ( !go.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\t// Skip player objects\r\n\t\tif ( IsPlayerObject( go ) )\r\n\t\t\treturn;\r\n\r\n\t\tif ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )\r\n\t\t\treturn;\r\n\r\n\t\t_baselineObjectIds.Add( go.Id );\r\n\r\n\t\tvar serialized = go.Serialize();\r\n\t\tif ( serialized is not null )\r\n\t\t{\r\n\t\t\t_baselineObjectData[go.Id] = serialized.ToJsonString();\r\n\t\t}\r\n\r\n\t\tforeach ( var child in go.Children?.ToArray() ?? [] )\r\n\t\t{\r\n\t\t\tCaptureObjectRecursive( child );\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Determines if a GameObject is a player or belongs to a player.\r\n\t/// </summary>\r\n\tprivate static bool IsPlayerObject( GameObject go )\r\n\t{\r\n\t\tif ( !go.IsValid() )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( go.Components.Get<Player>( true ) is not null )\r\n\t\t\treturn true;\r\n\r\n\t\tif ( go.Components.Get<PlayerData>( true ) is not null )\r\n\t\t\treturn true;\r\n\r\n\t\tvar parent = go.Parent;\r\n\t\twhile ( parent is not null && parent != go.Scene )\r\n\t\t{\r\n\t\t\tif ( parent.Components.Get<Player>( true ) is not null )\r\n\t\t\t\treturn true;\r\n\t\t\tif ( parent.Components.Get<PlayerData>( true ) is not null )\r\n\t\t\t\treturn true;\r\n\t\t\tparent = parent.Parent;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Cleans up the scene by removing all spawned objects and restoring destroyed baseline objects.\r\n\t/// Players and their belongings are preserved.\r\n\t/// </summary>\r\n\tpublic void Cleanup()\r\n\t{\r\n\t\tif ( !HasBaseline )\r\n\t\t{\r\n\t\t\tLog.Warning( \"CleanupSystem: No baseline captured. Cannot cleanup.\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !Networking.IsHost )\r\n\t\t{\r\n\t\t\tLog.Warning( \"CleanupSystem: Only the host can perform cleanup.\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar removedCount = 0;\r\n\t\tvar restoredCount = 0;\r\n\t\tvar objectsToRemove = new List<GameObject>();\r\n\t\tvar existingBaselineIds = new HashSet<Guid>();\r\n\r\n\t\tforeach ( var go in Scene.GetAllObjects( true ) )\r\n\t\t{\r\n\t\t\tif ( !go.IsValid() )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Never remove player objects\r\n\t\t\tif ( IsPlayerObject( go ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( go.Flags.Contains( GameObjectFlags.DontDestroyOnLoad ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( _baselineObjectIds.Contains( go.Id ) )\r\n\t\t\t{\r\n\t\t\t\texistingBaselineIds.Add( go.Id );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tif ( go.Parent == Scene )\r\n\t\t\t\t{\r\n\t\t\t\t\tobjectsToRemove.Add( go );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Remove spawned objects\r\n\t\tforeach ( var go in objectsToRemove )\r\n\t\t{\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tgo.Destroy();\r\n\t\t\t\tremovedCount++;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Restore destroyed baseline objects\r\n\t\tforeach ( var kvp in _baselineObjectData )\r\n\t\t{\r\n\t\t\tvar id = kvp.Key;\r\n\r\n\t\t\t// Skip if the object still exists\r\n\t\t\tif ( existingBaselineIds.Contains( id ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Skip if we already processed the parent object\r\n\t\t\tvar go = Scene.Directory.FindByGuid( id );\r\n\t\t\tif ( go.IsValid() )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tvar json = System.Text.Json.Nodes.JsonNode.Parse( kvp.Value );\r\n\t\t\t\tif ( json is System.Text.Json.Nodes.JsonObject jso )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar restored = new GameObject();\r\n\t\t\t\t\trestored.Deserialize( jso );\r\n\t\t\t\t\trestoredCount++;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tcatch ( System.Exception ex )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\"CleanupSystem: Failed to restore object {id}: {ex.Message}\" );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tBroadcastCleanup( removedCount, restoredCount );\r\n\t}\r\n\r\n\t[Rpc.Broadcast( NetFlags.HostOnly )]\r\n\tprivate static void BroadcastCleanup( int removedObjects, int restoredObjects )\r\n\t{\r\n\t\tGame.ActiveScene?.RunEvent<ICleanupEvents>( x => x.OnCleanup( removedObjects, restoredObjects ) );\r\n\r\n\t\tLog.Info( $\"Cleanup complete. Removed {removedObjects} spawned objects, restored {restoredObjects} destroyed objects.\" );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Console command to cleanup the map.\r\n\t/// </summary>\r\n\t[ConCmd( \"cleanup\" )]\r\n\tpublic static void CleanupCommand( string targetName = null )\r\n\t{\r\n\t\tif ( !Networking.IsHost ) return;\r\n\r\n\t\t//\r\n\t\t// Targeted cleanup, doesn't use the same cleanup shit\r\n\t\t//\r\n\t\tif ( !string.IsNullOrEmpty( targetName ) )\r\n\t\t{\r\n\t\t\tvar target = GameManager.FindPlayerWithName( targetName );\r\n\t\t\tif ( target is not null )\r\n\t\t\t{\r\n\t\t\t\tCleanupPlayer( target );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tNotices.AddNotice( \"cleaning_services\", Color.Red, $\"Can't find {targetName} to clean up\" );\r\n\t\t\t}\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( Current is null )\r\n\t\t{\r\n\t\t\tLog.Warning( \"CleanupSystem: No active cleanup system.\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tCurrent.Cleanup();\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tpublic static void RpcCleanUpMine()\r\n\t{\r\n\t\tCleanupPlayer( Rpc.Caller );\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tpublic static void RpcCleanUpAll()\r\n\t{\r\n\t\tif ( !Rpc.Caller.HasPermission( \"admin\" ) ) return;\r\n\r\n\t\tCurrent?.Cleanup();\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tpublic static void RpcCleanUpTarget( Connection target )\r\n\t{\r\n\t\tif ( !Rpc.Caller.HasPermission( \"admin\" ) ) return;\r\n\r\n\t\tCleanupPlayer( target );\r\n\t}\r\n\r\n\tpublic static void CleanupPlayer( Connection caller )\r\n\t{\r\n\t\tAssert.True( Networking.IsHost, \"Only the host may call this method!\" );\r\n\r\n\t\tvar removable = Game.ActiveScene.GetAllComponents<Ownable>()\r\n\t\t\t.Where( o => o.Owner == caller );\r\n\r\n\t\tvar count = 0;\r\n\t\tforeach ( var ownable in removable.ToArray() )\r\n\t\t{\r\n\t\t\townable.GameObject.Destroy();\r\n\t\t\tcount++;\r\n\t\t}\r\n\r\n\t\tNotices.SendNotice( caller, \"cleaning_services\", Color.Green, $\"Cleaned up {count} objects\" );\r\n\t}\r\n\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Game/Entity/DynamiteEntity.cs",
            "FileName": "DynamiteEntity.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "[Alias( \"dynamite\" )]\r\npublic sealed class DynamiteEntity : Component, IPlayerControllable, Component.IDamageable\r\n{\r\n\t[Property, Range( 1, 500 ), Step( 1 ), ClientEditable]\r\n\tpublic float Damage { get; set; } = 128;\r\n\r\n\t[Property, Range( 16, 4096 ), Step( 16 ), ClientEditable]\r\n\tpublic float Radius { get; set; } = 1024f;\r\n\r\n\t[Property, Range( 1, 100 ), Step( 1 ), ClientEditable]\r\n\tpublic float Force { get; set; } = 1;\r\n\r\n\t[Property, Sync, ClientEditable]\r\n\tpublic ClientInput Activate { get; set; }\r\n\r\n\tbool _isDead = false;\r\n\r\n\t[Rpc.Host]\r\n\tpublic void Explode()\r\n\t{\r\n\t\t_isDead = true;\r\n\r\n\t\tvar explosionPrefab = ResourceLibrary.Get<PrefabFile>( \"/prefabs/engine/explosion_med.prefab\" );\r\n\t\tif ( explosionPrefab == null )\r\n\t\t{\r\n\t\t\tLog.Warning( \"Can't find /prefabs/engine/explosion_med.prefab\" );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar go = GameObject.Clone( explosionPrefab, new CloneConfig { Transform = WorldTransform.WithScale( 1 ), StartEnabled = false } );\r\n\t\tif ( !go.IsValid() ) return;\r\n\r\n\t\tgo.RunEvent<RadiusDamage>( x =>\r\n\t\t{\r\n\t\t\tx.Radius = Radius;\r\n\t\t\tx.PhysicsForceScale = Force;\r\n\t\t\tx.DamageAmount = Damage;\r\n\t\t\tx.Attacker = go;\r\n\t\t}, FindMode.EverythingInSelfAndDescendants );\r\n\r\n\t\tgo.Enabled = true;\r\n\t\tgo.NetworkSpawn( true, null );\r\n\r\n\t\tGameObject.Destroy();\r\n\t}\r\n\r\n\tvoid IDamageable.OnDamage( in DamageInfo damage )\r\n\t{\r\n\t\tif ( _isDead ) return;\r\n\t\tif ( IsProxy ) return;\r\n\r\n\t\tExplode();\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnControl()\r\n\t{\r\n\t\tif ( Activate.Pressed() )\r\n\t\t{\r\n\t\t\tExplode();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnEndControl()\r\n\t{\r\n\t\t// nothing to do\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnStartControl()\r\n\t{\r\n\t\t// nothing to do\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Game/Entity/EmitterEntity.cs",
            "FileName": "EmitterEntity.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "/// <summary>\r\n/// Whether the emitter fires while the input is held, or toggles on/off with a press.\r\n/// </summary>\r\npublic enum EmitMode\r\n{\r\n\t/// <summary>\r\n\t/// Press once to turn on, press again to turn off.\r\n\t/// </summary>\r\n\tToggle,\r\n\t/// <summary>\r\n\t/// Emits only while the input is held down.\r\n\t/// </summary>\r\n\tHold,\r\n}\r\n\r\n/// <summary>\r\n/// A world-placed SENT that spawns and controls a particle/VFX emitter.\r\n/// The emitter prefab is defined by a <see cref=\"ScriptedEmitter\"/> resource.\r\n/// </summary>\r\n[Alias( \"emitter\" )]\r\npublic sealed class EmitterEntity : Component, IPlayerControllable\r\n{\r\n\t/// <summary>\r\n\t/// The emitter definition points to a prefab containing a particle system.\r\n\t/// </summary>\r\n\t[Property, ClientEditable]\r\n\tpublic ScriptedEmitter Emitter { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// Whether this emitter toggles on/off with a press, or emits only while held.\r\n\t/// </summary>\r\n\t[Property, ClientEditable]\r\n\tpublic EmitMode Mode { get; set; } = EmitMode.Toggle;\r\n\r\n\t/// <summary>\r\n\t/// Used when <see cref=\"Mode\"/> is <see cref=\"EmitMode.Toggle\"/>.\r\n\t/// </summary>\r\n\t[Property, Sync, ClientEditable, Group( \"Input\" )]\r\n\tpublic ClientInput ToggleInput { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// Used when <see cref=\"Mode\"/> is <see cref=\"EmitMode.Hold\"/>.\r\n\t/// </summary>\r\n\t[Property, Sync, ClientEditable, Group( \"Input\" )]\r\n\tpublic ClientInput HoldInput { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// Whether the emitter is currently active. Synced to all clients.\r\n\t/// </summary>\r\n\t[Sync] public bool IsEmitting { get; private set; }\r\n\r\n\t/// <summary>\r\n\t/// When enabled, forces the emitter on regardless of input or mode.\r\n\t/// Can be set from the editor or wired up externally.\r\n\t/// </summary>\r\n\t[Property, ClientEditable]\r\n\tpublic bool ManualOn\r\n\t{\r\n\t\tget => _manualOn;\r\n\t\tset { _manualOn = value; if ( !IsProxy ) UpdateEmitState(); }\r\n\t}\r\n\tprivate bool _manualOn;\r\n\tprivate bool _inputEmitting;\r\n\r\n\tprivate GameObject _particleInstance;\r\n\tprivate ScriptedEmitter _lastEmitter;\r\n\r\n\tprotected override void OnStart() { }\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// Emitter resource changed \u2014 destroy existing instance so it gets recreated\r\n\t\tif ( _lastEmitter != Emitter && _particleInstance.IsValid() )\r\n\t\t\tDestroyParticle();\r\n\r\n\t\t_lastEmitter = Emitter;\r\n\r\n\t\tif ( IsEmitting && !_particleInstance.IsValid() )\r\n\t\t\tSpawnParticle();\r\n\t\telse if ( !IsEmitting && _particleInstance.IsValid() )\r\n\t\t\tDestroyParticle();\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnStartControl() { }\r\n\tvoid IPlayerControllable.OnEndControl()\r\n\t{\r\n\t\tif ( Mode == EmitMode.Hold )\r\n\t\t{\r\n\t\t\t_inputEmitting = false;\r\n\t\t\tUpdateEmitState();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnControl()\r\n\t{\r\n\t\tif ( Mode == EmitMode.Toggle )\r\n\t\t{\r\n\t\t\tif ( ToggleInput.Pressed() )\r\n\t\t\t{\r\n\t\t\t\t_inputEmitting = !_inputEmitting;\r\n\t\t\t\tUpdateEmitState();\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar held = HoldInput.Down();\r\n\t\t\tif ( held != _inputEmitting )\r\n\t\t\t{\r\n\t\t\t\t_inputEmitting = held;\r\n\t\t\t\tUpdateEmitState();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void UpdateEmitState() => SetEmitting( _inputEmitting || _manualOn );\r\n\r\n\t[Rpc.Broadcast]\r\n\tprivate void SetEmitting( bool active )\r\n\t{\r\n\t\tIsEmitting = active;\r\n\t}\r\n\r\n\tprivate void SpawnParticle()\r\n\t{\r\n\t\tif ( !Emitter.IsValid() || Emitter.Prefab is null ) return;\r\n\r\n\t\t_particleInstance = GameObject.Clone( Emitter.Prefab, new CloneConfig\r\n\t\t{\r\n\t\t\tParent = GameObject,\r\n\t\t\tTransform = new Transform( Vector3.Forward * 4f ),\r\n\t\t\tStartEnabled = true,\r\n\t\t} );\r\n\t}\r\n\r\n\tprivate void DestroyParticle()\r\n\t{\r\n\t\t_particleInstance.Destroy();\r\n\t\t_particleInstance = null;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Game/Entity/SpotLightEntity.cs",
            "FileName": "SpotLightEntity.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "\r\n\r\npublic sealed class SpotLightEntity : Component, IPlayerControllable\r\n{\r\n\t[Property, ClientEditable, Group( \"Light\" )]\r\n\tpublic bool On { get; set { field = value; UpdateLight(); } } = true;\r\n\r\n\t[Property, ClientEditable, Group( \"Light\" )]\r\n\tpublic bool Shadows { get; set { field = value; UpdateLight(); } } = true;\r\n\r\n\t[Property, Range( 0, 1 ), ClientEditable, Group( \"Light\" )]\r\n\tpublic Color Color { get; set { field = value; UpdateLight(); } }\r\n\r\n\t[Property, Range( 0, 50 ), ClientEditable, Group( \"Light\" )]\r\n\tpublic float Brightness { get; set { field = value; UpdateLight(); } } = 2;\r\n\r\n\t[Property, Range( 0, 1000 ), ClientEditable, Group( \"Light\" )]\r\n\tpublic float Radius { get; set { field = value; UpdateLight(); } } = 500;\r\n\r\n\t[Property, Range( 0, 90 ), ClientEditable, Group( \"Light\" )]\r\n\tpublic float Angle { get; set { field = value; UpdateLight(); } } = 35;\r\n\r\n\t[Property, Range( 0, 16 ), ClientEditable, Group( \"Light\" )]\r\n\tpublic float Attenuation { get; set { field = value; UpdateLight(); } } = 2.4f;\r\n\r\n\r\n\t[Property, Sync, ClientEditable, Group( \"State\" )]\r\n\tpublic ClientInput TurnOn { get; set; }\r\n\r\n\t[Property, Sync, ClientEditable, Group( \"State\" )]\r\n\tpublic ClientInput TurnOff { get; set; }\r\n\r\n\t[Property, Sync, ClientEditable, Group( \"State\" )]\r\n\tpublic ClientInput Toggle { get; set; }\r\n\r\n\t[Property]\r\n\tpublic GameObject OnGameObject { get; set; }\r\n\r\n\t[Property]\r\n\tpublic GameObject OffGameObject { get; set; }\r\n\r\n\tvoid IPlayerControllable.OnControl()\r\n\t{\r\n\r\n\t\tif ( Toggle.Pressed() )\r\n\t\t{\r\n\t\t\tOn = !On;\r\n\t\t}\r\n\r\n\t\tif ( TurnOn.Pressed() )\r\n\t\t{\r\n\t\t\tOn = true;\r\n\t\t}\r\n\r\n\t\tif ( TurnOff.Pressed() )\r\n\t\t{\r\n\t\t\tOn = false;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnEndControl()\r\n\t{\r\n\r\n\t}\r\n\r\n\tvoid IPlayerControllable.OnStartControl()\r\n\t{\r\n\r\n\t}\r\n\r\n\tvoid UpdateLight()\r\n\t{\r\n\t\tOnGameObject?.Enabled = On;\r\n\t\tOffGameObject?.Enabled = !On;\r\n\r\n\t\tif ( GetComponentInChildren<SpotLight>( true ) is not SpotLight light )\r\n\t\t\treturn;\r\n\r\n\t\tlight.Enabled = On;\r\n\r\n\t\tvar color = Color;\r\n\t\tcolor.r *= Brightness;\r\n\t\tcolor.g *= Brightness;\r\n\t\tcolor.b *= Brightness;\r\n\r\n\t\tlight.Shadows = Shadows;\r\n\t\tlight.LightColor = color;\r\n\t\tlight.Radius = Radius;\r\n\t\tlight.Attenuation = Attenuation;\r\n\t\tlight.ConeOuter = Angle;\r\n\t\tlight.ConeInner = Angle * 0.5f;\r\n\r\n\t\tNetwork.Refresh();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Game/Weapon/BaseBulletWeapon/BaseBulletWeapon.cs",
            "FileName": "BaseBulletWeapon.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "public partial class BaseBulletWeapon : BaseWeapon\r\n{\r\n\t[Property]\r\n\tpublic SoundEvent ShootSound { get; set; }\r\n\r\n\t[Property, Group( \"Bullet\" )]\r\n\tpublic BulletConfiguration Bullet { get; set; } = new()\r\n\t{\r\n\t\tDamage = 12f,\r\n\t\tBulletRadius = 1f,\r\n\t\tRange = 4096f,\r\n\t\tAimConeBase = new Vector2( 0.5f, 0.25f ),\r\n\t\tAimConeSpread = new Vector2( 3f, 3f ),\r\n\t\tAimConeRecovery = 0.2f,\r\n\t\tRecoilPitch = new Vector2( -0.3f, -0.1f ),\r\n\t\tRecoilYaw = new Vector2( -0.1f, 0.1f ),\r\n\t\tCameraRecoilStrength = 1f,\r\n\t\tCameraRecoilFrequency = 1f,\r\n\t};\r\n\r\n\t[Property, Group( \"Bullet\" ), ClientEditable, Range( 0f, 500000f ), Step( 10f )]\r\n\tpublic float ShootForce { get; set; } = 100000f;\r\n\r\n\tprotected TimeSince TimeSinceShoot = 0;\r\n\r\n\t/// <summary>\r\n\t/// Returns 0 for no aim spread, 1 for full aim cone, based on time since last shot.\r\n\t/// </summary>\r\n\tprotected float GetAimConeAmount( float recovery )\r\n\t{\r\n\t\treturn TimeSinceShoot.Relative.Remap( 0, recovery, 1, 0 );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Returns the aim cone amount using the configured recovery time\r\n\t/// </summary>\r\n\tprotected float GetAimConeAmount()\r\n\t{\r\n\t\treturn GetAimConeAmount( Bullet.AimConeRecovery );\r\n\t}\r\n\r\n\t/// <inheritdoc cref=\"ShootBullet(float, in BulletConfiguration)\"/>\r\n\tprotected void ShootBullet( float fireRate )\r\n\t{\r\n\t\tShootBullet( fireRate, Bullet );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Shoot a bullet out of the front of the gun.\r\n\t/// When held by a player, fires from the player's eye with aim cone and recoil.\r\n\t/// When standalone (no owner), fires straight from the weapon's muzzle.\r\n\t/// </summary>\r\n\tprotected void ShootBullet( float fireRate, in BulletConfiguration config )\r\n\t{\r\n\t\tif ( HasOwner && ( !HasAmmo() || IsReloading() ) )\r\n\t\t{\r\n\t\t\tTryAutoReload();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( TimeUntilNextShotAllowed > 0 )\r\n\t\t\treturn;\r\n\r\n\t\t// Only consume ammo when held by a player\r\n\t\tif ( HasOwner && !TakeAmmo( 1 ) )\r\n\t\t{\r\n\t\t\tAddShootDelay( 0.2f );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tAddShootDelay( fireRate );\r\n\r\n\t\tvar aimConeAmount = GetAimConeAmount( config.AimConeRecovery );\r\n\t\tvar forward = AimRay.Forward\r\n\t\t\t.WithAimCone(\r\n\t\t\t\tconfig.AimConeBase.x + aimConeAmount * config.AimConeSpread.x,\r\n\t\t\t\tconfig.AimConeBase.y + aimConeAmount * config.AimConeSpread.y\r\n\t\t\t);\r\n\t\tvar traceRay = AimRay with { Forward = forward };\r\n\r\n\t\tvar tr = Scene.Trace.Ray( traceRay, config.Range )\r\n\t\t\t.IgnoreGameObjectHierarchy( AimIgnoreRoot )\r\n\t\t\t.WithCollisionRules( \"bullet\" )\r\n\t\t\t.WithoutTags( \"playercontroller\" )\r\n\t\t\t.Radius( config.BulletRadius )\r\n\t\t\t.UseHitboxes()\r\n\t\t\t.Run();\r\n\r\n\t\tShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject, tr.Surface );\r\n\t\tTraceAttack( TraceAttackInfo.From( tr, config.Damage ) );\r\n\t\tTimeSinceShoot = 0;\r\n\r\n\t\t// Recoil only applies when held by a player\r\n\t\tif ( !HasOwner )\r\n\t\t{\r\n\t\t\t// Simulate physical recoil by pushing the weapon opposite to its fire direction\r\n\t\t\tif ( ShootForce > 0f && GetComponent<Rigidbody>( true ) is var rb )\r\n\t\t\t{\r\n\t\t\t\tvar muzzle = WeaponModel?.MuzzleTransform?.WorldTransform ?? WorldTransform;\r\n\t\t\t\trb.ApplyForce( muzzle.Rotation.Up * ShootForce );\r\n\t\t\t}\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tOwner.Controller.EyeAngles += new Angles(\r\n\t\t\tRandom.Shared.Float( config.RecoilPitch.x, config.RecoilPitch.y ),\r\n\t\t\tRandom.Shared.Float( config.RecoilYaw.x, config.RecoilYaw.y ),\r\n\t\t\t0\r\n\t\t);\r\n\r\n\t\tif ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )\r\n\t\t{\r\n\t\t\t_ = new Sandbox.CameraNoise.Recoil( config.CameraRecoilStrength, config.CameraRecoilFrequency );\r\n\t\t}\r\n\t}\r\n\r\n\t[Rpc.Broadcast]\r\n\tpublic void ShootEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject, Surface hitSurface, Vector3? origin = null, bool noEvents = false )\r\n\t{\r\n\t\tif ( Application.IsDedicatedServer ) return;\r\n\t\tif ( !hitSurface.IsValid() ) return;\r\n\r\n\t\tOwner?.Controller.Renderer.Set( \"b_attack\", true );\r\n\r\n\t\tif ( !noEvents )\r\n\t\t{\r\n\t\t\tif ( WeaponModel.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tWeaponModel.GameObject.RunEvent<WeaponModel>( x => x.OnAttack() );\r\n\t\t\t\tWeaponModel.GameObject.RunEvent<WeaponModel>( x => x.CreateRangedEffects( this, hitpoint, origin ) );\r\n\t\t\t}\r\n\r\n\t\t\tif ( ShootSound.IsValid() )\r\n\t\t\t{\r\n\t\t\t\tvar snd = GameObject.PlaySound( ShootSound );\r\n\r\n\t\t\t\t// If we're shooting, the sound should not be spatialized\r\n\t\t\t\tif ( HasOwner && Owner.IsLocalPlayer && snd.IsValid() )\r\n\t\t\t\t{\r\n\t\t\t\t\tsnd.SpacialBlend = 0;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !hit || !hitObject.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar baseSurface = hitSurface.GetBaseSurface();\r\n\t\tvar bulletSound = hitSurface.SoundCollection.Bullet ?? baseSurface?.SoundCollection.Bullet;\r\n\t\tif ( bulletSound.IsValid() )\r\n\t\t{\r\n\t\t\tSound.Play( bulletSound, hitpoint );\r\n\t\t}\r\n\r\n\t\tvar prefab = hitSurface.PrefabCollection.BulletImpact ?? baseSurface?.PrefabCollection.BulletImpact;\r\n\r\n\t\t// Still null?\r\n\t\tif ( prefab is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar fwd = Rotation.LookAt( normal * -1.0f, Vector3.Random );\r\n\r\n\t\tvar impact = prefab.Clone();\r\n\t\timpact.WorldPosition = hitpoint;\r\n\t\timpact.WorldRotation = fwd;\r\n\t\timpact.SetParent( hitObject, true );\r\n\r\n\t\tif ( hitObject.GetComponentInChildren<SkinnedModelRenderer>() is not { CreateBoneObjects: true } skinned )\r\n\t\t\treturn;\r\n\r\n\t\t// find closest bone\r\n\t\tvar bones = skinned.GetBoneTransforms( true );\r\n\r\n\t\tvar closestDist = float.MaxValue;\r\n\r\n\t\tfor ( var i = 0; i < bones.Length; i++ )\r\n\t\t{\r\n\t\t\tvar bone = bones[i];\r\n\t\t\tvar dist = bone.Position.Distance( hitpoint );\r\n\t\t\tif ( dist < closestDist )\r\n\t\t\t{\r\n\t\t\t\tclosestDist = dist;\r\n\t\t\t\timpact.SetParent( skinned.GetBoneObject( i ), true );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic record struct BulletConfiguration\r\n\t{\r\n\t\tpublic float Damage { get; set; }\r\n\t\tpublic float BulletRadius { get; set; }\r\n\t\tpublic Vector2 AimConeBase { get; set; }\r\n\t\tpublic Vector2 AimConeSpread { get; set; }\r\n\t\tpublic float AimConeRecovery { get; set; }\r\n\t\tpublic Vector2 RecoilPitch { get; set; }\r\n\t\tpublic Vector2 RecoilYaw { get; set; }\r\n\t\tpublic float CameraRecoilStrength { get; set; }\r\n\t\tpublic float CameraRecoilFrequency { get; set; }\r\n\t\tpublic float Range { get; set; }\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Game/Weapon/BaseWeapon/BaseWeapon.Reloading.cs",
            "FileName": "BaseWeapon.Reloading.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "using System.Threading;\r\n\r\npublic partial class BaseWeapon\r\n{\r\n\t/// <summary>\r\n\t/// Should we consume 1 bullet per reload instead of filling the clip?\r\n\t/// </summary>\r\n\t[Property, Feature( \"Ammo\" )]\r\n\tpublic bool IncrementalReloading { get; set; } = false;\r\n\r\n\t/// <summary>\r\n\t/// Extra delay after the first shell reload before subsequent shells begin (e.g. longer carrier insertion animation).\r\n\t/// Only used with incremental reloading. If zero, no extra delay is added.\r\n\t/// </summary>\r\n\t[Property, Feature( \"Ammo\" ), ShowIf( nameof( IncrementalReloading ), true )]\r\n\tpublic float FirstShellReloadTime { get; set; } = 0f;\r\n\r\n\t/// <summary>\r\n\t/// Delay before the first shell is inserted during incremental reload.\r\n\t/// If zero, uses <see cref=\"ReloadTime\"/>.\r\n\t/// </summary>\r\n\t[Property, Feature( \"Ammo\" ), ShowIf( nameof( IncrementalReloading ), true )]\r\n\tpublic float ReloadStartTime { get; set; } = 0f;\r\n\r\n\t/// <summary>\r\n\t/// Can we cancel reloads?\r\n\t/// </summary>\r\n\t[Property, Feature( \"Ammo\" )]\r\n\tpublic bool CanCancelReload { get; set; } = true;\r\n\r\n\tprivate CancellationTokenSource reloadToken;\r\n\tprivate bool isReloading;\r\n\r\n\tpublic bool CanReload()\r\n\t{\r\n\t\tif ( !UsesClips ) return false;\r\n\t\tif ( ClipContents >= ClipMaxSize ) return false;\r\n\t\tif ( isReloading ) return false;\r\n\t\tif ( !WeaponConVars.InfiniteReserves && ReserveAmmo <= 0 ) return false;\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tpublic bool IsReloading() => isReloading;\r\n\r\n\tpublic virtual void CancelReload()\r\n\t{\r\n\t\tif ( reloadToken?.IsCancellationRequested == false )\r\n\t\t{\r\n\t\t\treloadToken?.Cancel();\r\n\t\t\tisReloading = false;\r\n\r\n\t\t\tViewModel?.RunEvent<ViewModel>( x => x.OnReloadCancel() );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic virtual async void OnReloadStart()\r\n\t{\r\n\t\tif ( !CanReload() )\r\n\t\t\treturn;\r\n\r\n\t\tCancelReload();\r\n\r\n\t\tvar cts = new CancellationTokenSource();\r\n\t\treloadToken = cts;\r\n\t\tisReloading = true;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tawait ReloadAsync( cts.Token );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t// Only clean up our own reload\r\n\t\t\tif ( reloadToken == cts )\r\n\t\t\t{\r\n\t\t\t\tisReloading = false;\r\n\t\t\t\treloadToken = null;\r\n\t\t\t}\r\n\t\t\tcts.Dispose();\r\n\t\t}\r\n\t}\r\n\r\n\t[Rpc.Broadcast]\r\n\tprivate void BroadcastReload()\r\n\t{\r\n\t\tif ( !HasOwner ) return;\r\n\r\n\t\tAssert.True( Owner.Controller.IsValid(), \"BaseWeapon::BroadcastReload - Player Controller is invalid!\" );\r\n\t\tAssert.True( Owner.Controller.Renderer.IsValid(), \"BaseWeapon::BroadcastReload - Renderer is invalid!\" );\r\n\r\n\t\tOwner.Controller.Renderer.Set( \"b_reload\", true );\r\n\t}\r\n\r\n\tprotected virtual async Task ReloadAsync( CancellationToken ct )\r\n\t{\r\n\t\t// Capture so we can tell if a newer reload has replaced us by the time finally runs.\r\n\t\tvar mySource = reloadToken;\r\n\t\tvar isFirstShell = ClipContents == 0;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tViewModel?.RunEvent<ViewModel>( x => x.OnReloadStart() );\r\n\r\n\t\t\tBroadcastReload();\r\n\r\n\t\t\tvar firstIteration = true;\r\n\r\n\t\t\twhile ( ClipContents < ClipMaxSize && !ct.IsCancellationRequested )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar delay = (firstIteration && IncrementalReloading && ReloadStartTime > 0f) ? ReloadStartTime : ReloadTime;\r\n\t\t\t\t\tfirstIteration = false;\r\n\t\t\t\t\tawait Task.DelaySeconds( delay, ct );\r\n\r\n\t\t\t\t\tvar needed = IncrementalReloading ? 1 : (ClipMaxSize - ClipContents);\r\n\r\n\t\t\t\t\tif ( WeaponConVars.InfiniteReserves )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );\r\n\t\t\t\t\t\tClipContents += needed;\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar available = Math.Min( needed, ReserveAmmo );\r\n\r\n\t\t\t\t\t\tif ( available <= 0 )\r\n\t\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\t\tViewModel?.RunEvent<ViewModel>( x => x.OnIncrementalReload( isFirstShell ) );\r\n\r\n\t\t\t\t\t\tReserveAmmo -= available;\r\n\t\t\t\t\t\tClipContents += available;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t// After the first shell, wait longer before the next one starts\r\n\t\t\t\t\tif ( isFirstShell && FirstShellReloadTime > 0f )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tawait Task.DelaySeconds( FirstShellReloadTime, ct );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tisFirstShell = false;\r\n\t\t\t\t}\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\tif ( reloadToken == mySource )\r\n\t\t\t{\r\n\t\t\t\tViewModel?.RunEvent<ViewModel>( x => x.OnReloadFinish() );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "GameLoop/GamePreferences.cs",
            "FileName": "GamePreferences.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "/// <summary>\r\n/// The local user's preferences in Deathmatch\r\n/// </summary>\r\ninternal static class GamePreferences\r\n{\r\n\t/// <summary>\r\n\t/// Enables automatic switching to better weapons on item pickup\r\n\t/// </summary>\r\n\t[ConVar( \"sb.autoswitch\", ConVarFlags.UserInfo | ConVarFlags.Saved )]\r\n\tpublic static bool AutoSwitch { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// Enables fast switching between inventory weapons\r\n\t/// </summary>\r\n\t[ConVar( \"sb.fastswitch\", ConVarFlags.Saved )]\r\n\tpublic static bool FastSwitch { get; set; } = false;\r\n\r\n\t/// <summary>\r\n\t/// Intensity of your camera's screenshake\r\n\t/// </summary>\r\n\t[ConVar( \"sb.viewbob\", ConVarFlags.Saved )]\r\n\t[Group( \"Camera\" )]\r\n\tpublic static bool ViewBobbing { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// Intensity of your camera's screenshake\r\n\t/// </summary>\r\n\t[ConVar( \"sb.screenshake\", ConVarFlags.Saved )]\r\n\t[Range( 0.1f, 2f ), Step( 0.1f ), Group( \"Camera\" )]\r\n\tpublic static float Screenshake { get; set; } = 0.3f;\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Npcs/NpcConVars.cs",
            "FileName": "NpcConVars.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "namespace Sandbox.Npcs;\r\n\r\n/// <summary>\r\n/// Console variables that control NPC AI behaviour globally.\r\n/// </summary>\r\npublic static class NpcConVars\r\n{\r\n\t/// <summary>\r\n\t/// When disabled, all NPC AI thinking is paused \u2014 they just stand idle.\r\n\t/// </summary>\r\n\t[ConVar( \"sb.ai.enabled\", ConVarFlags.Replicated | ConVarFlags.Saved, Help = \"Enable or disable NPC AI thinking.\" )]\r\n\tpublic static bool Enabled { get; set; } = true;\r\n\r\n\t/// <summary>\r\n\t/// When enabled, NPCs cannot target players.\r\n\t/// </summary>\r\n\t[ConVar( \"sb.ai.notarget\", ConVarFlags.Replicated | ConVarFlags.Saved, Help = \"When enabled, NPCs cannot target players.\" )]\r\n\tpublic static bool NoTarget { get; set; } = false;\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Npcs/Scientist/ScientistFleeSchedule.cs",
            "FileName": "ScientistFleeSchedule.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "using Sandbox.Npcs.Layers;\r\nusing Sandbox.Npcs.Tasks;\r\n\r\nnamespace Sandbox.Npcs.Schedules;\r\n\r\n/// <summary>\r\n/// Panic flee \u2014 scream while sprinting away from the source.\r\n/// </summary>\r\npublic sealed class ScientistFleeSchedule : ScheduleBase\r\n{\r\n\tprivate static readonly string[] PanicLines =\r\n\t[\r\n\t\t\"AHHH!\",\r\n\t\t\"Don't hurt me!\",\r\n\t\t\"Help! HELP!\",\r\n\t\t\"Stay away from me!\",\r\n\t\t\"I'm just a scientist!\",\r\n\t\t\"Please, no!\",\r\n\t\t\"Somebody help!\",\r\n\t\t\"Oh god oh god oh god!\",\r\n\t\t\"What did I do?!\",\r\n\t\t\"Leave me alone!\",\r\n\t];\r\n\r\n\tpublic GameObject Source { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// 0\u20131 panic intensity. Higher values mean faster speed and longer flee distance.\r\n\t/// </summary>\r\n\tpublic float PanicLevel { get; set; } = 0.5f;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tif ( !Source.IsValid() ) return;\r\n\r\n\t\t// Sprint speed scales with panic (200\u2013350)\r\n\t\tNpc.Navigation.WishSpeed = 200f + 150f * PanicLevel;\r\n\r\n\t\t// Don't stare at the player \u2014 look where we're running\r\n\t\tNpc.Animation.ClearLookTarget();\r\n\r\n\t\t// Scream immediately \u2014 but only if not already mid-speech\r\n\t\tif ( Npc.Speech.CanSpeak )\r\n\t\t{\r\n\t\t\tvar line = PanicLines[Game.Random.Int( 0, PanicLines.Length - 1 )];\r\n\t\t\tNpc.Speech.Say( line, 2f );\r\n\t\t}\r\n\r\n\t\t// Flee direction \u2014 away from the attacker with some randomness\r\n\t\tvar awayDir = (GameObject.WorldPosition - Source.WorldPosition).WithZ( 0 ).Normal;\r\n\t\tvar randomAngle = Game.Random.Float( -40f, 40f );\r\n\t\tawayDir = Rotation.FromAxis( Vector3.Up, randomAngle ) * awayDir;\r\n\r\n\t\t// Distance scales with panic (200\u2013500)\r\n\t\tvar fleeDist = 512f + 1024f * PanicLevel;\r\n\t\tvar fleeTarget = GameObject.WorldPosition + awayDir * fleeDist;\r\n\r\n\t\t// Snap to navmesh\r\n\t\tif ( Npc.Scene.NavMesh.GetClosestPoint( fleeTarget ) is { } navPoint )\r\n\t\t{\r\n\t\t\tAddTask( new MoveTo( navPoint, 15f ) );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tAddTask( new MoveTo( fleeTarget, 15f ) );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnEnd()\r\n\t{\r\n\t\t// Reset to normal walk speed\r\n\t\t// TODO: this is shit, can we scope these somehow so the IDisposable handles all this ?\r\n\t\tNpc.Navigation.WishSpeed = 100f;\r\n\t}\r\n\r\n\tprotected override bool ShouldCancel()\r\n\t{\r\n\t\treturn !Source.IsValid();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Player/PlayerFallDamage.cs",
            "FileName": "PlayerFallDamage.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "/// <summary>\r\n/// Apply fall damage to the player\r\n/// </summary>\r\npublic class PlayerFallDamage : Component, Local.IPlayerEvents\r\n{\r\n\t[RequireComponent] public Player Player { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// Fatal fall speed, you will die if you fall at or above this speed\r\n\t/// </summary>\r\n\t[Property] public float FatalFallSpeed { get; set; } = 1536.0f;\r\n\r\n\t/// <summary>\r\n\t/// Maximum safe fall speed, you won't take damage at or below this speed\r\n\t/// </summary>\r\n\t[Property] public float MaxSafeFallSpeed { get; set; } = 512.0f;\r\n\r\n\t/// <summary>\r\n\t/// Multiply damage amount by this much\r\n\t/// </summary>\r\n\t[Property] public float DamageMultiplier { get; set; } = 1.0f;\r\n\r\n\t/// <summary>\r\n\t/// Fall damage sound\r\n\t/// </summary>\r\n\t[Property] public SoundEvent FallSound { get; set; }\r\n\r\n\t[Rpc.Owner]\r\n\tprivate void PlayFallSound()\r\n\t{\r\n\t\tGameObject.PlaySound( FallSound );\r\n\t}\r\n\r\n\tvoid Local.IPlayerEvents.OnLand( float distance, Vector3 velocity )\r\n\t{\r\n\t\tvar fallSpeed = Math.Abs( velocity.z );\r\n\r\n\t\tif ( fallSpeed <= MaxSafeFallSpeed )\r\n\t\t\treturn;\r\n\r\n\t\tvar damageAmount = MathX.Remap( fallSpeed, MaxSafeFallSpeed, FatalFallSpeed, 0f, 100f ) * DamageMultiplier;\r\n\t\tif ( damageAmount < 1 ) return;\r\n\r\n\t\tif ( Networking.IsHost && damageAmount >= Player.Health )\r\n\t\t\tPlayer.PlayerData?.AddStat( \"player.fall.death\" );\r\n\r\n\t\tTakeFallDamage( damageAmount );\r\n\t}\r\n\r\n\r\n\t[Rpc.Broadcast]\r\n\tpublic void TakeFallDamage( float amount )\r\n\t{\r\n\t\tif ( !Networking.IsHost ) return;\r\n\r\n\r\n\t\tif ( Player is IDamageable damage )\r\n\t\t{\r\n\t\t\tvar dmg = new DamageInfo( amount.CeilToInt(), Player.GameObject, null );\r\n\t\t\tdmg.Tags.Add( DamageTags.Fall );\r\n\t\t\tdamage.OnDamage( dmg );\r\n\r\n\t\t\tPlayFallSound();\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Player/PlayerLoadout.cs",
            "FileName": "PlayerLoadout.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "/// <summary>\r\n/// Manages loadout persistence, presets, and restoration for a player.\r\n/// Lives on the Player GameObject alongside PlayerInventory.\r\n/// Listens to inventory events to auto-save, and handles all loadout RPCs directly.\r\n/// </summary>\r\npublic sealed class PlayerLoadout : Component, Local.IPlayerEvents, Global.IPlayerEvents, Global.ISaveEvents\r\n{\r\n\t[RequireComponent] public Player Player { get; set; }\r\n\t[RequireComponent] public PlayerInventory Inventory { get; set; }\r\n\r\n\tprivate bool _isRestoringLoadout;\r\n\r\n\t/// <summary>\r\n\t/// One entry in a serialized loadout: the prefab resource path and the slot it occupies.\r\n\t/// </summary>\r\n\tpublic struct LoadoutEntry\r\n\t{\r\n\t\tpublic string PrefabPath { get; set; }\r\n\t\tpublic int Slot { get; set; }\r\n\t\tpublic string SpawnerDataPayload { get; set; }\r\n\t}\r\n\r\n\tpublic struct SavedPreset\r\n\t{\r\n\t\tpublic string Name { get; set; }\r\n\t\tpublic string LoadoutJson { get; set; }\r\n\t}\r\n\r\n\tpublic static IReadOnlyList<SavedPreset> GetLoadoutPresets()\r\n\t{\r\n\t\treturn LocalData.Get<List<SavedPreset>>( \"presets\", new() );\r\n\t}\r\n\r\n\tpublic static void SaveLoadoutPreset( string name, string loadoutJson )\r\n\t{\r\n\t\tvar presets = LocalData.Get<List<SavedPreset>>( \"presets\", new() );\r\n\t\tvar idx = presets.FindIndex( p => p.Name == name );\r\n\t\tvar entry = new SavedPreset { Name = name, LoadoutJson = loadoutJson };\r\n\t\tif ( idx >= 0 )\r\n\t\t\tpresets[idx] = entry;\r\n\t\telse\r\n\t\t\tpresets.Add( entry );\r\n\t\tLocalData.Set( \"presets\", presets );\r\n\t}\r\n\r\n\tpublic static void DeleteLoadoutPreset( string name )\r\n\t{\r\n\t\tvar presets = LocalData.Get<List<SavedPreset>>( \"presets\", new() );\r\n\t\tpresets.RemoveAll( p => p.Name == name );\r\n\t\tLocalData.Set( \"presets\", presets );\r\n\t}\r\n\r\n\tpublic string SerializeLoadout()\r\n\t{\r\n\t\tvar entries = Inventory.Weapons\r\n\t\t\t.Where( w => !string.IsNullOrEmpty( w.GameObject.PrefabInstanceSource ) )\r\n\t\t\t.Select( w => new LoadoutEntry\r\n\t\t\t{\r\n\t\t\t\tPrefabPath = w.GameObject.PrefabInstanceSource,\r\n\t\t\t\tSlot = w.InventorySlot,\r\n\t\t\t\tSpawnerDataPayload = (w as SpawnerWeapon)?.SpawnerData\r\n\t\t\t} )\r\n\t\t\t.ToList();\r\n\r\n\t\treturn entries.Count > 0 ? Json.Serialize( entries ) : null;\r\n\t}\r\n\r\n\tpublic void SaveLoadout()\r\n\t{\r\n\t\tif ( _isRestoringLoadout ) return;\r\n\r\n\t\tvar json = SerializeLoadout();\r\n\t\tif ( string.IsNullOrEmpty( json ) ) return;\r\n\r\n\t\tif ( Player.IsLocalPlayer )\r\n\t\t{\r\n\t\t\tLocalData.Set( \"hotbar\", json );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tPushLoadoutToClient( json );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void GiveLoadoutWeapons( string json )\r\n\t{\r\n\t\tvar entries = Json.Deserialize<List<LoadoutEntry>>( json );\r\n\t\tif ( entries is null ) return;\r\n\r\n\t\t_isRestoringLoadout = true;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tforeach ( var entry in entries )\r\n\t\t\t{\r\n\t\t\t\tif ( !Inventory.Pickup( entry.PrefabPath, entry.Slot, false ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tif ( !string.IsNullOrEmpty( entry.SpawnerDataPayload ) && Inventory.GetSlot( entry.Slot ) is SpawnerWeapon spawnerWeapon )\r\n\t\t\t\t{\r\n\t\t\t\t\tspawnerWeapon.RestoreSpawnerData( entry.SpawnerDataPayload );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_isRestoringLoadout = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static async Task EnsureMountedAsync( string json )\r\n\t{\r\n\t\tvar entries = Json.Deserialize<List<LoadoutEntry>>( json );\r\n\t\tif ( entries is null ) return;\r\n\r\n\t\tvar needsMounts = entries.Any( e => !string.IsNullOrEmpty( e.SpawnerDataPayload )\r\n\t\t\t&& e.SpawnerDataPayload.EndsWith( \".vmdl\", StringComparison.OrdinalIgnoreCase ) );\r\n\r\n\t\tif ( !needsMounts ) return;\r\n\r\n\t\tforeach ( var entry in Sandbox.Mounting.Directory.GetAll().Where( e => e.Available ) )\r\n\t\t\tawait Sandbox.Mounting.Directory.Mount( entry.Ident );\r\n\t}\r\n\r\n\tpublic void SwitchToPreset( string loadoutJson )\r\n\t{\r\n\t\tif ( !Networking.IsHost )\r\n\t\t{\r\n\t\t\tHostSwitchToPreset( loadoutJson );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\t_ = SwitchToPresetAsync( loadoutJson );\r\n\t}\r\n\r\n\tpublic void ResetToDefault()\r\n\t{\r\n\t\tif ( !Networking.IsHost )\r\n\t\t{\r\n\t\t\tHostResetToDefault();\r\n\t\t\treturn;\r\n\t\t}\r\n\t\t_ = ResetToDefaultAsync();\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tprivate void HostSwitchToPreset( string loadoutJson )\r\n\t{\r\n\t\t_ = SwitchToPresetAsync( loadoutJson );\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tprivate void HostResetToDefault()\r\n\t{\r\n\t\t_ = ResetToDefaultAsync();\r\n\t}\r\n\r\n\tprivate async Task SwitchToPresetAsync( string loadoutJson )\r\n\t{\r\n\t\tvar previousSlot = Inventory.ActiveWeapon?.InventorySlot ?? 0;\r\n\r\n\t\tforeach ( var weapon in Inventory.Weapons.ToList() )\r\n\t\t\tweapon.DestroyGameObject();\r\n\r\n\t\tawait Task.Yield();\r\n\r\n\t\tawait EnsureMountedAsync( loadoutJson );\r\n\t\tGiveLoadoutWeapons( loadoutJson );\r\n\r\n\t\tvar toEquip = Inventory.GetSlot( previousSlot ) ?? Inventory.GetBestWeapon();\r\n\t\tif ( toEquip.IsValid() )\r\n\t\t\tInventory.SwitchWeapon( toEquip );\r\n\r\n\t\tSaveLoadout();\r\n\t}\r\n\r\n\tprivate async Task ResetToDefaultAsync()\r\n\t{\r\n\t\tforeach ( var weapon in Inventory.Weapons.ToList() )\r\n\t\t\tweapon.DestroyGameObject();\r\n\r\n\t\tawait Task.Yield();\r\n\r\n\t\tInventory.GiveDefaultWeapons();\r\n\t\tInventory.SwitchWeapon( Inventory.GetBestWeapon() );\r\n\t\tSaveLoadout();\r\n\t}\r\n\r\n\t[Rpc.Owner]\r\n\tprivate void PushLoadoutToClient( string loadoutJson )\r\n\t{\r\n\t\tLocalData.Set( \"hotbar\", loadoutJson );\r\n\t}\r\n\r\n\t[Rpc.Owner]\r\n\tprivate void RequestClientLoadout()\r\n\t{\r\n\t\tvar json = LocalData.Get<string>( \"hotbar\" );\r\n\t\tif ( !string.IsNullOrEmpty( json ) )\r\n\t\t\tHostRestoreLoadoutFromClient( json );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Clears the current inventory, waits a frame, then gives the loadout from JSON and equips the best weapon.\r\n\t/// </summary>\r\n\tprivate async Task ReplaceLoadoutAsync( string json )\r\n\t{\r\n\t\tforeach ( var weapon in Inventory.Weapons.ToList() )\r\n\t\t\tweapon.DestroyGameObject();\r\n\r\n\t\tawait Task.Yield();\r\n\r\n\t\tawait EnsureMountedAsync( json );\r\n\t\tGiveLoadoutWeapons( json );\r\n\r\n\t\tvar best = Inventory.GetBestWeapon();\r\n\t\tif ( best.IsValid() )\r\n\t\t\tInventory.SwitchWeapon( best );\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tprivate async void HostRestoreLoadoutFromClient( string loadoutJson )\r\n\t{\r\n\t\tawait ReplaceLoadoutAsync( loadoutJson );\r\n\t}\r\n\r\n\tvoid Global.IPlayerEvents.OnPlayerSpawned( Player player )\r\n\t{\r\n\t\tif ( player != Player ) return;\r\n\t\tif ( !Networking.IsHost ) return;\r\n\r\n\t\t_ = RestoreOnSpawnAsync();\r\n\t}\r\n\r\n\tprivate async Task RestoreOnSpawnAsync()\r\n\t{\r\n\t\tif ( Player.IsLocalPlayer )\r\n\t\t{\r\n\t\t\tvar json = LocalData.Get<string>( \"hotbar\" );\r\n\t\t\tif ( !string.IsNullOrEmpty( json ) )\r\n\t\t\t{\r\n\t\t\t\tawait ReplaceLoadoutAsync( json );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tRequestClientLoadout();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tInventory.GiveDefaultWeapons();\r\n\t\tvar bestWeapon = Inventory.GetBestWeapon();\r\n\t\tif ( bestWeapon.IsValid() )\r\n\t\t\tInventory.SwitchWeapon( bestWeapon );\r\n\t}\r\n\r\n\tvoid Local.IPlayerEvents.OnDied( PlayerDiedParams args )\r\n\t{\r\n\t\tif ( !Networking.IsHost ) return;\r\n\t\tSaveLoadout();\r\n\t}\r\n\r\n\tvoid Local.IPlayerEvents.OnPickup( PlayerPickupEvent e )\r\n\t{\r\n\t\tif ( e.Cancelled ) return;\r\n\t\tif ( !Networking.IsHost ) return;\r\n\t\tSaveLoadout();\r\n\t}\r\n\r\n\tvoid Local.IPlayerEvents.OnDrop( PlayerDropEvent e )\r\n\t{\r\n\t\tif ( e.Cancelled ) return;\r\n\t\tif ( !Networking.IsHost ) return;\r\n\t\t_ = SaveLoadoutAfterYield();\r\n\t}\r\n\r\n\tvoid Local.IPlayerEvents.OnRemoveWeapon( PlayerRemoveWeaponEvent e )\r\n\t{\r\n\t\tif ( e.Cancelled ) return;\r\n\t\tif ( !Networking.IsHost ) return;\r\n\t\t_ = SaveLoadoutAfterYield();\r\n\t}\r\n\r\n\tvoid Local.IPlayerEvents.OnMoveSlot( PlayerMoveSlotEvent e )\r\n\t{\r\n\t\tif ( e.Cancelled ) return;\r\n\t\tif ( !Networking.IsHost ) return;\r\n\t\tSaveLoadout();\r\n\t}\r\n\r\n\tprivate async Task SaveLoadoutAfterYield()\r\n\t{\r\n\t\tawait Task.Yield();\r\n\t\tSaveLoadout();\r\n\t}\r\n\r\n\tvoid Global.ISaveEvents.BeforeSave( string filename )\r\n\t{\r\n\t\tif ( !Networking.IsHost ) return;\r\n\r\n\t\tvar steamId = (long)(Player.Network.Owner?.SteamId ?? 0);\r\n\t\tif ( steamId == 0 ) return;\r\n\r\n\t\tvar json = SerializeLoadout();\r\n\t\tif ( string.IsNullOrEmpty( json ) ) return;\r\n\r\n\t\tSaveSystem.Current?.SetMetadata( $\"Loadout_{steamId}\", json );\r\n\t}\r\n\r\n\tvoid Global.ISaveEvents.AfterLoad( string filename )\r\n\t{\r\n\t\tif ( !Networking.IsHost ) return;\r\n\r\n\t\tvar steamId = (long)(Player.Network.Owner?.SteamId ?? 0);\r\n\t\tif ( steamId == 0 ) return;\r\n\r\n\t\tvar json = SaveSystem.Current?.GetMetadata( $\"Loadout_{steamId}\" );\r\n\t\tif ( string.IsNullOrEmpty( json ) ) return;\r\n\r\n\t\t_ = RestoreLoadoutFromSaveAsync( json );\r\n\t}\r\n\r\n\tprivate async Task RestoreLoadoutFromSaveAsync( string json )\r\n\t{\r\n\t\tawait ReplaceLoadoutAsync( json );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "Player/PlayerObserver.cs",
            "FileName": "PlayerObserver.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "/// <summary>\r\n/// Dead players become these. They try to observe their last corpse. \r\n/// </summary>\r\ninternal sealed class PlayerObserver : Component\r\n{\r\n\tAngles EyeAngles;\r\n\tTimeSince timeSinceStarted;\r\n\tDeathCameraTarget _cachedCorpse;\r\n\tfloat currentDistance;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tbase.OnEnabled();\r\n\r\n\t\tEyeAngles = Scene.Camera.WorldRotation;\r\n\t\ttimeSinceStarted = 0;\r\n\t\tcurrentDistance = 32;\r\n\r\n\t\t_cachedCorpse = Scene.GetAllComponents<DeathCameraTarget>()\r\n\t\t\t\t\t.Where( x => x.Connection == Network.Owner )\r\n\t\t\t\t\t.OrderByDescending( x => x.Created )\r\n\t\t\t\t\t.FirstOrDefault();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// Don't allow immediate respawn\r\n\t\tif ( timeSinceStarted < 1 )\r\n\t\t\treturn;\r\n\r\n\t\t// If pressed a button, or has been too long\r\n\t\tif ( Input.Pressed( \"attack1\" ) || Input.Pressed( \"jump\" ) || timeSinceStarted > 4f )\r\n\t\t{\r\n\t\t\tGameManager.Current?.RequestRespawn();\r\n\t\t\tGameObject.Destroy();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnPreRender()\r\n\t{\r\n\t\tif ( IsProxy ) return;\r\n\r\n\t\tif ( _cachedCorpse.IsValid() )\r\n\t\t{\r\n\t\t\tRotateAround( _cachedCorpse );\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void RotateAround( Component target )\r\n\t{\r\n\t\t// Find the corpse eyes\r\n\t\tif ( target.Components.Get<SkinnedModelRenderer>().TryGetBoneTransform( \"pelvis\", out var tx ) )\r\n\t\t{\r\n\t\t\ttx.Position += Vector3.Up * 25;\r\n\t\t}\r\n\r\n\t\tvar e = EyeAngles;\r\n\t\te += Input.AnalogLook;\r\n\t\te.pitch = e.pitch.Clamp( -90, 90 );\r\n\t\te.roll = 0.0f;\r\n\t\tEyeAngles = e;\r\n\r\n\t\tcurrentDistance = currentDistance.LerpTo( 150, Time.Delta * 5 );\r\n\r\n\t\tvar center = tx.Position;\r\n\t\tvar targetPos = center - EyeAngles.Forward * currentDistance;\r\n\r\n\t\tvar tr = Scene.Trace.FromTo( center, targetPos ).Radius( 1.0f ).WithoutTags( \"ragdoll\", \"effect\" ).Run();\r\n\r\n\t\tScene.Camera.WorldPosition = tr.EndPosition;\r\n\t\tScene.Camera.WorldRotation = EyeAngles;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "softsplit.sandbox",
            "Path": "UI/Components/SpawnMenuContent.razor",
            "FileName": "SpawnMenuContent.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 303259,
            "Code": "@using Sandbox;\r\n@using Sandbox.UI;\r\n@inherits Panel\r\n@namespace Sandbox\r\n\r\n<root>\r\n    @if (Header != null)\r\n    {\r\n        <div class=\"header\">@Header</div>\r\n    }\r\n\r\n    <div class=\"body menuinner\">@Body</div>\r\n</root>\r\n\r\n@code\r\n{\r\n\r\n    [Parameter] public RenderFragment Header { get; set; }\r\n    [Parameter] public RenderFragment Body { get; set; }\r\n\r\n}\r\n"
        }
    ]
}