🔍 s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
🔗 Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=softsplit.sandbox&take=20
Showing code results for query:
*
(420 total matches found)
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;
}
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;
}
}
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;
}
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;
}
}
}
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";
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;
}
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;
}
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" );
}
}
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
}
}
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;
}
}
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();
}
}
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; }
}
}
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() );
}
}
}
}
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;
}
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;
}
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();
}
}
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();
}
}
}
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 );
}
}
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;
}
}
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"
}
]
}