{"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\u0026:hover, \u0026: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\u0026:active\r\n\t{\r\n\t\tcolor: white;\r\n\t}\r\n}\r\n\r\nColorControl \u003E .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: \u0022Material Icons\u0022;\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\u0026:hover\r\n\t{\r\n\t\topacity: 1;\r\n\t\tbackground-color: #fff;\r\n\t}\r\n\r\n\t\u0026: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\u0026.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\u0026: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\u0026.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n\r\nControlSheetRow \u003E .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 \u003E .right\r\n{\r\n\tflex-grow: 1;\r\n}\r\n\r\nControlSheetRow \u003E .left \u003E .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 \u003E .right \u003E textentry,\r\nControlSheetRow \u003E .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 \u0022/UI/Theme.scss\u0022;\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\u0026.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: \u0022Poppins\u0022;\r\n$body-font: \u0022Inter\u0022;\r\n$subtitle-font: \u0022Poppins\u0022;\r\n\r\n@import \u0022/UI/Menu.scss\u0022;\r\n@import \u0022/UI/Hud.scss\u0022;\r\n"},{"Ident":"softsplit.sandbox","Path":"ui/faceposer/faceposeeditor.razor.scss","FileName":"faceposeeditor.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":303259,"Code":"@import \u0022/UI/Theme.scss\u0022;\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\u0026:hover\r\n\t{\r\n\t\tcolor: $menu-text-strong;\r\n\t}\r\n\r\n\t\u0026.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\u0026:hover\r\n\t\t{\r\n\t\t\topacity: 0.7;\r\n\t\t}\r\n\r\n\t\t\u0026.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\u0026.grow\r\n\t{\r\n\t\tgap: 8px;\r\n\r\n\t\t\u003E *\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\u0026.disabled\r\n\t{\r\n\t\tpointer-events: none;\r\n\t\topacity: 0.25;\r\n\t}\r\n\r\n\t\u0026.soft\r\n\t{\r\n\t\tbackground-color: $menu-accent-soft;\r\n\r\n\t\t\u0026: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\u0026.primary\r\n\t{\r\n\t\tborder-color: $menu-accent;\r\n\r\n\t\t\u0026: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\u0026.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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\ninternal sealed class CleanupSystem : GameObjectSystem\u003CCleanupSystem\u003E, ISceneLoadingEvents\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Set of GameObjects that existed in the original scene baseline.\r\n\t/// \u003C/summary\u003E\r\n\tprivate readonly HashSet\u003CGuid\u003E _baselineObjectIds = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Serialized data of baseline objects so we can restore them if destroyed.\r\n\t/// \u003C/summary\u003E\r\n\tprivate readonly Dictionary\u003CGuid, string\u003E _baselineObjectData = new();\r\n\r\n\tprivate static bool _restorePersistedBaseline;\r\n\tprivate static HashSet\u003CGuid\u003E _persistedBaselineIds;\r\n\tprivate static Dictionary\u003CGuid, string\u003E _persistedBaselineData;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether a baseline has been captured.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool HasBaseline =\u003E _baselineObjectIds.Count \u003E 0;\r\n\r\n\tpublic CleanupSystem( Scene scene ) : base( scene )\r\n\t{\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Call from SaveSystem before Game.ChangeScene() to snapshot the current baseline\r\n\t/// \u003C/summary\u003E\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\u003CGuid\u003E( Current._baselineObjectIds );\r\n\t\t_persistedBaselineData = new Dictionary\u003CGuid, string\u003E( 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\u0027t 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 \u0026\u0026 _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( $\u0022CleanupSystem: Restored persisted baseline with {_baselineObjectIds.Count} objects.\u0022 );\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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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( $\u0022CleanupSystem: Captured baseline with {_baselineObjectIds.Count} objects.\u0022 );\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/// \u003Csummary\u003E\r\n\t/// Determines if a GameObject is a player or belongs to a player.\r\n\t/// \u003C/summary\u003E\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\u003CPlayer\u003E( true ) is not null )\r\n\t\t\treturn true;\r\n\r\n\t\tif ( go.Components.Get\u003CPlayerData\u003E( 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 \u0026\u0026 parent != go.Scene )\r\n\t\t{\r\n\t\t\tif ( parent.Components.Get\u003CPlayer\u003E( true ) is not null )\r\n\t\t\t\treturn true;\r\n\t\t\tif ( parent.Components.Get\u003CPlayerData\u003E( 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/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\tpublic void Cleanup()\r\n\t{\r\n\t\tif ( !HasBaseline )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022CleanupSystem: No baseline captured. Cannot cleanup.\u0022 );\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( \u0022CleanupSystem: Only the host can perform cleanup.\u0022 );\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\u003CGameObject\u003E();\r\n\t\tvar existingBaselineIds = new HashSet\u003CGuid\u003E();\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\u002B\u002B;\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\u002B\u002B;\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( $\u0022CleanupSystem: Failed to restore object {id}: {ex.Message}\u0022 );\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\u003CICleanupEvents\u003E( x =\u003E x.OnCleanup( removedObjects, restoredObjects ) );\r\n\r\n\t\tLog.Info( $\u0022Cleanup complete. Removed {removedObjects} spawned objects, restored {restoredObjects} destroyed objects.\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Console command to cleanup the map.\r\n\t/// \u003C/summary\u003E\r\n\t[ConCmd( \u0022cleanup\u0022 )]\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\u0027t 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( \u0022cleaning_services\u0022, Color.Red, $\u0022Can\u0027t find {targetName} to clean up\u0022 );\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( \u0022CleanupSystem: No active cleanup system.\u0022 );\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( \u0022admin\u0022 ) ) 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( \u0022admin\u0022 ) ) 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, \u0022Only the host may call this method!\u0022 );\r\n\r\n\t\tvar removable = Game.ActiveScene.GetAllComponents\u003COwnable\u003E()\r\n\t\t\t.Where( o =\u003E 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\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tNotices.SendNotice( caller, \u0022cleaning_services\u0022, Color.Green, $\u0022Cleaned up {count} objects\u0022 );\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( \u0022dynamite\u0022 )]\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\u003CPrefabFile\u003E( \u0022/prefabs/engine/explosion_med.prefab\u0022 );\r\n\t\tif ( explosionPrefab == null )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022Can\u0027t find /prefabs/engine/explosion_med.prefab\u0022 );\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\u003CRadiusDamage\u003E( x =\u003E\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":"/// \u003Csummary\u003E\r\n/// Whether the emitter fires while the input is held, or toggles on/off with a press.\r\n/// \u003C/summary\u003E\r\npublic enum EmitMode\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Press once to turn on, press again to turn off.\r\n\t/// \u003C/summary\u003E\r\n\tToggle,\r\n\t/// \u003Csummary\u003E\r\n\t/// Emits only while the input is held down.\r\n\t/// \u003C/summary\u003E\r\n\tHold,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A world-placed SENT that spawns and controls a particle/VFX emitter.\r\n/// The emitter prefab is defined by a \u003Csee cref=\u0022ScriptedEmitter\u0022/\u003E resource.\r\n/// \u003C/summary\u003E\r\n[Alias( \u0022emitter\u0022 )]\r\npublic sealed class EmitterEntity : Component, IPlayerControllable\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The emitter definition points to a prefab containing a particle system.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, ClientEditable]\r\n\tpublic ScriptedEmitter Emitter { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether this emitter toggles on/off with a press, or emits only while held.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, ClientEditable]\r\n\tpublic EmitMode Mode { get; set; } = EmitMode.Toggle;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Used when \u003Csee cref=\u0022Mode\u0022/\u003E is \u003Csee cref=\u0022EmitMode.Toggle\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Sync, ClientEditable, Group( \u0022Input\u0022 )]\r\n\tpublic ClientInput ToggleInput { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Used when \u003Csee cref=\u0022Mode\u0022/\u003E is \u003Csee cref=\u0022EmitMode.Hold\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Sync, ClientEditable, Group( \u0022Input\u0022 )]\r\n\tpublic ClientInput HoldInput { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether the emitter is currently active. Synced to all clients.\r\n\t/// \u003C/summary\u003E\r\n\t[Sync] public bool IsEmitting { get; private set; }\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Property, ClientEditable]\r\n\tpublic bool ManualOn\r\n\t{\r\n\t\tget =\u003E _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 \u0026\u0026 _particleInstance.IsValid() )\r\n\t\t\tDestroyParticle();\r\n\r\n\t\t_lastEmitter = Emitter;\r\n\r\n\t\tif ( IsEmitting \u0026\u0026 !_particleInstance.IsValid() )\r\n\t\t\tSpawnParticle();\r\n\t\telse if ( !IsEmitting \u0026\u0026 _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() =\u003E 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( \u0022Light\u0022 )]\r\n\tpublic bool On { get; set { field = value; UpdateLight(); } } = true;\r\n\r\n\t[Property, ClientEditable, Group( \u0022Light\u0022 )]\r\n\tpublic bool Shadows { get; set { field = value; UpdateLight(); } } = true;\r\n\r\n\t[Property, Range( 0, 1 ), ClientEditable, Group( \u0022Light\u0022 )]\r\n\tpublic Color Color { get; set { field = value; UpdateLight(); } }\r\n\r\n\t[Property, Range( 0, 50 ), ClientEditable, Group( \u0022Light\u0022 )]\r\n\tpublic float Brightness { get; set { field = value; UpdateLight(); } } = 2;\r\n\r\n\t[Property, Range( 0, 1000 ), ClientEditable, Group( \u0022Light\u0022 )]\r\n\tpublic float Radius { get; set { field = value; UpdateLight(); } } = 500;\r\n\r\n\t[Property, Range( 0, 90 ), ClientEditable, Group( \u0022Light\u0022 )]\r\n\tpublic float Angle { get; set { field = value; UpdateLight(); } } = 35;\r\n\r\n\t[Property, Range( 0, 16 ), ClientEditable, Group( \u0022Light\u0022 )]\r\n\tpublic float Attenuation { get; set { field = value; UpdateLight(); } } = 2.4f;\r\n\r\n\r\n\t[Property, Sync, ClientEditable, Group( \u0022State\u0022 )]\r\n\tpublic ClientInput TurnOn { get; set; }\r\n\r\n\t[Property, Sync, ClientEditable, Group( \u0022State\u0022 )]\r\n\tpublic ClientInput TurnOff { get; set; }\r\n\r\n\t[Property, Sync, ClientEditable, Group( \u0022State\u0022 )]\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\u003CSpotLight\u003E( 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( \u0022Bullet\u0022 )]\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( \u0022Bullet\u0022 ), 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/// \u003Csummary\u003E\r\n\t/// Returns 0 for no aim spread, 1 for full aim cone, based on time since last shot.\r\n\t/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// Returns the aim cone amount using the configured recovery time\r\n\t/// \u003C/summary\u003E\r\n\tprotected float GetAimConeAmount()\r\n\t{\r\n\t\treturn GetAimConeAmount( Bullet.AimConeRecovery );\r\n\t}\r\n\r\n\t/// \u003Cinheritdoc cref=\u0022ShootBullet(float, in BulletConfiguration)\u0022/\u003E\r\n\tprotected void ShootBullet( float fireRate )\r\n\t{\r\n\t\tShootBullet( fireRate, Bullet );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\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\u0027s eye with aim cone and recoil.\r\n\t/// When standalone (no owner), fires straight from the weapon\u0027s muzzle.\r\n\t/// \u003C/summary\u003E\r\n\tprotected void ShootBullet( float fireRate, in BulletConfiguration config )\r\n\t{\r\n\t\tif ( HasOwner \u0026\u0026 ( !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 \u003E 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 \u0026\u0026 !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 \u002B aimConeAmount * config.AimConeSpread.x,\r\n\t\t\t\tconfig.AimConeBase.y \u002B 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( \u0022bullet\u0022 )\r\n\t\t\t.WithoutTags( \u0022playercontroller\u0022 )\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 \u003E 0f \u0026\u0026 GetComponent\u003CRigidbody\u003E( 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 \u002B= 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 \u0026\u0026 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( \u0022b_attack\u0022, 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\u003CWeaponModel\u003E( x =\u003E x.OnAttack() );\r\n\t\t\t\tWeaponModel.GameObject.RunEvent\u003CWeaponModel\u003E( x =\u003E 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\u0027re shooting, the sound should not be spatialized\r\n\t\t\t\tif ( HasOwner \u0026\u0026 Owner.IsLocalPlayer \u0026\u0026 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\u003CSkinnedModelRenderer\u003E() 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 \u003C bones.Length; i\u002B\u002B )\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 \u003C 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/// \u003Csummary\u003E\r\n\t/// Should we consume 1 bullet per reload instead of filling the clip?\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature( \u0022Ammo\u0022 )]\r\n\tpublic bool IncrementalReloading { get; set; } = false;\r\n\r\n\t/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\n\t[Property, Feature( \u0022Ammo\u0022 ), ShowIf( nameof( IncrementalReloading ), true )]\r\n\tpublic float FirstShellReloadTime { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Delay before the first shell is inserted during incremental reload.\r\n\t/// If zero, uses \u003Csee cref=\u0022ReloadTime\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature( \u0022Ammo\u0022 ), ShowIf( nameof( IncrementalReloading ), true )]\r\n\tpublic float ReloadStartTime { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Can we cancel reloads?\r\n\t/// \u003C/summary\u003E\r\n\t[Property, Feature( \u0022Ammo\u0022 )]\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 \u003E= ClipMaxSize ) return false;\r\n\t\tif ( isReloading ) return false;\r\n\t\tif ( !WeaponConVars.InfiniteReserves \u0026\u0026 ReserveAmmo \u003C= 0 ) return false;\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tpublic bool IsReloading() =\u003E 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\u003CViewModel\u003E( x =\u003E 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(), \u0022BaseWeapon::BroadcastReload - Player Controller is invalid!\u0022 );\r\n\t\tAssert.True( Owner.Controller.Renderer.IsValid(), \u0022BaseWeapon::BroadcastReload - Renderer is invalid!\u0022 );\r\n\r\n\t\tOwner.Controller.Renderer.Set( \u0022b_reload\u0022, 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\u003CViewModel\u003E( x =\u003E 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 \u003C ClipMaxSize \u0026\u0026 !ct.IsCancellationRequested )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar delay = (firstIteration \u0026\u0026 IncrementalReloading \u0026\u0026 ReloadStartTime \u003E 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\u003CViewModel\u003E( x =\u003E x.OnIncrementalReload( isFirstShell ) );\r\n\t\t\t\t\t\tClipContents \u002B= 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 \u003C= 0 )\r\n\t\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\t\tViewModel?.RunEvent\u003CViewModel\u003E( x =\u003E x.OnIncrementalReload( isFirstShell ) );\r\n\r\n\t\t\t\t\t\tReserveAmmo -= available;\r\n\t\t\t\t\t\tClipContents \u002B= 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 \u0026\u0026 FirstShellReloadTime \u003E 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\u003CViewModel\u003E( x =\u003E 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":"/// \u003Csummary\u003E\r\n/// The local user\u0027s preferences in Deathmatch\r\n/// \u003C/summary\u003E\r\ninternal static class GamePreferences\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Enables automatic switching to better weapons on item pickup\r\n\t/// \u003C/summary\u003E\r\n\t[ConVar( \u0022sb.autoswitch\u0022, ConVarFlags.UserInfo | ConVarFlags.Saved )]\r\n\tpublic static bool AutoSwitch { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Enables fast switching between inventory weapons\r\n\t/// \u003C/summary\u003E\r\n\t[ConVar( \u0022sb.fastswitch\u0022, ConVarFlags.Saved )]\r\n\tpublic static bool FastSwitch { get; set; } = false;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Intensity of your camera\u0027s screenshake\r\n\t/// \u003C/summary\u003E\r\n\t[ConVar( \u0022sb.viewbob\u0022, ConVarFlags.Saved )]\r\n\t[Group( \u0022Camera\u0022 )]\r\n\tpublic static bool ViewBobbing { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Intensity of your camera\u0027s screenshake\r\n\t/// \u003C/summary\u003E\r\n\t[ConVar( \u0022sb.screenshake\u0022, ConVarFlags.Saved )]\r\n\t[Range( 0.1f, 2f ), Step( 0.1f ), Group( \u0022Camera\u0022 )]\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/// \u003Csummary\u003E\r\n/// Console variables that control NPC AI behaviour globally.\r\n/// \u003C/summary\u003E\r\npublic static class NpcConVars\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// When disabled, all NPC AI thinking is paused \u2014 they just stand idle.\r\n\t/// \u003C/summary\u003E\r\n\t[ConVar( \u0022sb.ai.enabled\u0022, ConVarFlags.Replicated | ConVarFlags.Saved, Help = \u0022Enable or disable NPC AI thinking.\u0022 )]\r\n\tpublic static bool Enabled { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// When enabled, NPCs cannot target players.\r\n\t/// \u003C/summary\u003E\r\n\t[ConVar( \u0022sb.ai.notarget\u0022, ConVarFlags.Replicated | ConVarFlags.Saved, Help = \u0022When enabled, NPCs cannot target players.\u0022 )]\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/// \u003Csummary\u003E\r\n/// Panic flee \u2014 scream while sprinting away from the source.\r\n/// \u003C/summary\u003E\r\npublic sealed class ScientistFleeSchedule : ScheduleBase\r\n{\r\n\tprivate static readonly string[] PanicLines =\r\n\t[\r\n\t\t\u0022AHHH!\u0022,\r\n\t\t\u0022Don\u0027t hurt me!\u0022,\r\n\t\t\u0022Help! HELP!\u0022,\r\n\t\t\u0022Stay away from me!\u0022,\r\n\t\t\u0022I\u0027m just a scientist!\u0022,\r\n\t\t\u0022Please, no!\u0022,\r\n\t\t\u0022Somebody help!\u0022,\r\n\t\t\u0022Oh god oh god oh god!\u0022,\r\n\t\t\u0022What did I do?!\u0022,\r\n\t\t\u0022Leave me alone!\u0022,\r\n\t];\r\n\r\n\tpublic GameObject Source { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// 0\u20131 panic intensity. Higher values mean faster speed and longer flee distance.\r\n\t/// \u003C/summary\u003E\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 \u002B 150f * PanicLevel;\r\n\r\n\t\t// Don\u0027t stare at the player \u2014 look where we\u0027re 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 \u002B 1024f * PanicLevel;\r\n\t\tvar fleeTarget = GameObject.WorldPosition \u002B 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":"/// \u003Csummary\u003E\r\n/// Apply fall damage to the player\r\n/// \u003C/summary\u003E\r\npublic class PlayerFallDamage : Component, Local.IPlayerEvents\r\n{\r\n\t[RequireComponent] public Player Player { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fatal fall speed, you will die if you fall at or above this speed\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public float FatalFallSpeed { get; set; } = 1536.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Maximum safe fall speed, you won\u0027t take damage at or below this speed\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public float MaxSafeFallSpeed { get; set; } = 512.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Multiply damage amount by this much\r\n\t/// \u003C/summary\u003E\r\n\t[Property] public float DamageMultiplier { get; set; } = 1.0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fall damage sound\r\n\t/// \u003C/summary\u003E\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 \u003C= 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 \u003C 1 ) return;\r\n\r\n\t\tif ( Networking.IsHost \u0026\u0026 damageAmount \u003E= Player.Health )\r\n\t\t\tPlayer.PlayerData?.AddStat( \u0022player.fall.death\u0022 );\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":"/// \u003Csummary\u003E\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/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n\t/// One entry in a serialized loadout: the prefab resource path and the slot it occupies.\r\n\t/// \u003C/summary\u003E\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\u003CSavedPreset\u003E GetLoadoutPresets()\r\n\t{\r\n\t\treturn LocalData.Get\u003CList\u003CSavedPreset\u003E\u003E( \u0022presets\u0022, 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\u003CList\u003CSavedPreset\u003E\u003E( \u0022presets\u0022, new() );\r\n\t\tvar idx = presets.FindIndex( p =\u003E p.Name == name );\r\n\t\tvar entry = new SavedPreset { Name = name, LoadoutJson = loadoutJson };\r\n\t\tif ( idx \u003E= 0 )\r\n\t\t\tpresets[idx] = entry;\r\n\t\telse\r\n\t\t\tpresets.Add( entry );\r\n\t\tLocalData.Set( \u0022presets\u0022, presets );\r\n\t}\r\n\r\n\tpublic static void DeleteLoadoutPreset( string name )\r\n\t{\r\n\t\tvar presets = LocalData.Get\u003CList\u003CSavedPreset\u003E\u003E( \u0022presets\u0022, new() );\r\n\t\tpresets.RemoveAll( p =\u003E p.Name == name );\r\n\t\tLocalData.Set( \u0022presets\u0022, 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 =\u003E !string.IsNullOrEmpty( w.GameObject.PrefabInstanceSource ) )\r\n\t\t\t.Select( w =\u003E 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 \u003E 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( \u0022hotbar\u0022, 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\u003CList\u003CLoadoutEntry\u003E\u003E( 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 ) \u0026\u0026 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\u003CList\u003CLoadoutEntry\u003E\u003E( json );\r\n\t\tif ( entries is null ) return;\r\n\r\n\t\tvar needsMounts = entries.Any( e =\u003E !string.IsNullOrEmpty( e.SpawnerDataPayload )\r\n\t\t\t\u0026\u0026 e.SpawnerDataPayload.EndsWith( \u0022.vmdl\u0022, 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 =\u003E 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( \u0022hotbar\u0022, 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\u003Cstring\u003E( \u0022hotbar\u0022 );\r\n\t\tif ( !string.IsNullOrEmpty( json ) )\r\n\t\t\tHostRestoreLoadoutFromClient( json );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Clears the current inventory, waits a frame, then gives the loadout from JSON and equips the best weapon.\r\n\t/// \u003C/summary\u003E\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\u003Cstring\u003E( \u0022hotbar\u0022 );\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( $\u0022Loadout_{steamId}\u0022, 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( $\u0022Loadout_{steamId}\u0022 );\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":"/// \u003Csummary\u003E\r\n/// Dead players become these. They try to observe their last corpse. \r\n/// \u003C/summary\u003E\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\u003CDeathCameraTarget\u003E()\r\n\t\t\t\t\t.Where( x =\u003E x.Connection == Network.Owner )\r\n\t\t\t\t\t.OrderByDescending( x =\u003E 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\u0027t allow immediate respawn\r\n\t\tif ( timeSinceStarted \u003C 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( \u0022attack1\u0022 ) || Input.Pressed( \u0022jump\u0022 ) || timeSinceStarted \u003E 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\u003CSkinnedModelRenderer\u003E().TryGetBoneTransform( \u0022pelvis\u0022, out var tx ) )\r\n\t\t{\r\n\t\t\ttx.Position \u002B= Vector3.Up * 25;\r\n\t\t}\r\n\r\n\t\tvar e = EyeAngles;\r\n\t\te \u002B= 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( \u0022ragdoll\u0022, \u0022effect\u0022 ).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\u003Croot\u003E\r\n    @if (Header != null)\r\n    {\r\n        \u003Cdiv class=\u0022header\u0022\u003E@Header\u003C/div\u003E\r\n    }\r\n\r\n    \u003Cdiv class=\u0022body menuinner\u0022\u003E@Body\u003C/div\u003E\r\n\u003C/root\u003E\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"}]}