Search the source of every open source package.
8849 results
using System;
using Sandbox;
namespace Goo.Animation;
public record struct SmoothVector2
{
public Vector2 Current;
public Vector2 Target;
public Vector2 Velocity;
public float SmoothTime;
public SmoothVector2(Vector2 initial, float smoothTime)
{
Current = initial;
Target = initial;
Velocity = default;
SmoothTime = smoothTime;
}
public void Update(float dt)
{
float vx = Velocity.x, vy = Velocity.y;
Current = new Vector2(
MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
Velocity = new Vector2(vx, vy);
}
public bool IsSettled =>
MathF.Abs(Target.x - Current.x) < 0.0001f &&
MathF.Abs(Target.y - Current.y) < 0.0001f &&
MathF.Abs(Velocity.x) < 0.0001f &&
MathF.Abs(Velocity.y) < 0.0001f;
/// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;
namespace Goo;
// Style helpers hoisted so generated Blob facades share them. Keep the early-return form: an engine-type ternary that resolves bare null via an implicit string operator silently produces magenta (Color.Parse fallback) instead of the intended absent-property. See engine-fact memories.
internal static class StyleAccumulator
{
static StyleList Rent(StyleList current)
=> ReferenceEquals(current, StyleList.Empty)
? BuildContext.Current.RentStyleList()
: current;
public static StyleList Add(StyleList current, StyleField field, Length? value)
{
if (!value.HasValue) return current;
var list = Rent(current);
list.Add(field, StyleValue.FromLength(value.Value));
return list;
}
public static StyleList Add<TEnum>(StyleList current, StyleField field, TEnum? value, System.Func<TEnum, StyleValue> wrap) where TEnum : struct
{
if (!value.HasValue) return current;
var list = Rent(current);
list.Add(field, wrap(value.Value));
return list;
}
public static StyleList Add(StyleList current, StyleField field, Color? value, System.Func<Color, StyleValue> wrap)
{
if (!value.HasValue) return current;
var list = Rent(current);
list.Add(field, wrap(value.Value));
return list;
}
public static StyleList Add(StyleList current, StyleField field, string? value)
{
if (value is null) return current;
var list = Rent(current);
list.Add(field, StyleValue.FromString(value));
return list;
}
public static StyleList Add(StyleList current, StyleField field, float? value)
{
if (!value.HasValue) return current;
var list = Rent(current);
list.Add(field, StyleValue.FromSingle(value.Value));
return list;
}
public static StyleList Add(StyleList current, StyleField field, bool? value)
{
if (!value.HasValue) return current;
var list = Rent(current);
list.Add(field, StyleValue.FromBoolean(value.Value));
return list;
}
public static StyleList Add(StyleList current, StyleField field, int? value)
{
if (!value.HasValue) return current;
var list = Rent(current);
list.Add(field, StyleValue.FromInt32(value.Value));
return list;
}
public static StyleList Add(StyleList current, StyleField field, Texture? value)
{
if (value is null) return current;
var list = Rent(current);
list.Add(field, StyleValue.FromTexture(value));
return list;
}
}
using System;
using Sandbox;
namespace Goo.FpsUI;
// ============================================================================
// ALL demo / self-preview behaviour for the FPS UI pack lives in this ONE file.
//
// DELETE THIS FILE to remove demo functionality.
// deleting this file makes every Demo inspector toggle disappear
// ============================================================================
static class FpsDemo
{
static readonly string[] Names = { "Vex", "Korr", "Juno", "Rhys", "Mara", "Dane", "Iko", "Sol" };
static readonly (int Amount, string Action)[] Grants =
{
(100, "Kill"), (125, "Headshot"), (50, "Assist"), (200, "Double Kill"), (25, "Hitmarker"),
};
public static void Health( HealthModel m, ref float t, float dt ) // bleed in chunks, big heal when low
{
t += dt;
if ( t < 2.4f ) return;
t = 0f;
if ( m.Health <= m.MaxHealth * 0.3f ) m.Heal( m.MaxHealth * 0.65f );
else m.Damage( m.MaxHealth * 0.22f );
}
public static void Stamina( StaminaModel m, ref float t, float dt ) // sprint on/off cycle
{
t += dt;
if ( t >= 3.1f ) { t = 0f; m.SetSprinting( !m.Sprinting ); }
if ( m.Stamina <= 0f ) m.SetSprinting( false );
}
public static void Ammo( AmmoModel m, ref float t, float dt ) // steady fire, auto-reload on empty
{
t += dt;
if ( m.Reloading ) return;
if ( m.Mag <= 0 ) { m.Reload(); return; }
if ( t >= 0.18f ) { t = 0f; m.Fire(); }
}
public static void Crosshair( CrosshairModel m, ref float t, float dt ) // steady fire cadence
{
t += dt;
if ( t >= 0.18f ) { t = 0f; m.Fire(); }
}
public static void Hitmarker( HitmarkerModel m, ref float t, float dt ) // periodic pop
{
t += dt;
if ( t >= 0.5f ) { t = 0f; m.Pop( false ); }
}
public static void Killfeed( KillfeedModel m, ref float t, ref int pick, float dt ) // periodic fake kills
{
t += dt;
if ( t < 2.2f ) return;
t = 0f;
string a = Names[pick % Names.Length];
string v = Names[(pick + 3) % Names.Length];
// Alternate teams, occasionally make the local player the killer so the preview shows both.
bool youKill = pick % 3 == 0;
KillTeam at = youKill || pick % 2 == 0 ? KillTeam.Friendly : KillTeam.Enemy;
KillTeam vt = at == KillTeam.Friendly ? KillTeam.Enemy : KillTeam.Friendly;
pick++;
m.Add( a, at, v, vt, attackerLocal: youKill );
}
public static void Xp( XpModel m, ref float t, ref int pick, float dt ) // periodic grants
{
t += dt;
if ( t < 1.6f ) return;
t = 0f;
var g = Grants[pick % Grants.Length];
pick++;
m.Add( g.Amount, g.Action );
}
public static void Scoreboard( ScoreboardModel m, ref float t, float dt ) // loop the clock, nudge scores, cycle variant
{
if ( m.TimeRemaining <= 0f ) m.TimeRemaining = 600f;
t += dt;
if ( t < 4f ) return;
t = 0f;
m.Mode = m.Mode switch
{
ScoreboardMode.Tdm => ScoreboardMode.Domination,
ScoreboardMode.Domination => ScoreboardMode.Ffa,
_ => ScoreboardMode.Tdm,
};
m.FriendlyScore = (m.FriendlyScore + 7) % (m.ScoreLimit + 1);
m.EnemyScore = (m.EnemyScore + 5) % (m.ScoreLimit + 1);
m.PlayerScore += 150;
m.LeaderScore = Math.Max( m.LeaderScore, m.PlayerScore ) + 50;
for ( int i = 0; i < m.Points.Length; i++ ) // rotate cap ownership
m.Points[i] = (CapOwner)(((int)m.Points[i] + 1) % 3);
}
}
// ---- per-widget demo hooks (the [Property] Demo toggle + timers + StepDemo body) ----
public sealed partial class HealthWidget
{
[Property] public bool Demo { get; set; } = true; // self-animate in editor
float _demoT;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Health( _m, ref _demoT, dt );
}
}
public sealed partial class StaminaWidget
{
[Property] public bool Demo { get; set; } = true; // self-animate in editor
float _demoT;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Stamina( _m, ref _demoT, dt );
}
}
public sealed partial class AmmoWidget
{
[Property] public bool Demo { get; set; } = true; // self-fire in editor
float _demoT;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Ammo( _m, ref _demoT, dt );
}
}
public sealed partial class CrosshairWidget
{
[Property] public bool Demo { get; set; } = true; // self-fire in editor
float _demoT;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Crosshair( _m, ref _demoT, dt );
}
}
public sealed partial class HitmarkerWidget
{
[Property] public bool Demo { get; set; } = true; // periodic pop in editor
float _demoT;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Hitmarker( _m, ref _demoT, dt );
}
}
public sealed partial class KillfeedWidget
{
[Property] public bool Demo { get; set; } = true; // fake kills in editor
float _demoT;
int _demoPick;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Killfeed( _m, ref _demoT, ref _demoPick, dt );
}
}
public sealed partial class XpWidget
{
[Property] public bool Demo { get; set; } = true; // fake grants in editor
float _demoT;
int _demoPick;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Xp( _m, ref _demoT, ref _demoPick, dt );
}
}
public sealed partial class ScoreboardWidget
{
[Property] public bool Demo { get; set; } = true; // self-animate in editor
float _demoT;
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
FpsDemo.Scoreboard( _m, ref _demoT, dt );
}
}
// ---- the assembled HUD: one coordinated synthetic firefight ----
public sealed partial class FpsHud
{
[Property, Group( "General" )] public bool Demo { get; set; } = true; // self-run a synthetic firefight in editor
readonly Random _demoRng = new();
float _hpT, _stT, _kfT, _xpT, _sbT; // per-subsystem demo timers
int _kfPick, _xpPick; // demo name / grant cursors
float _triggerT, _modeT; // synthetic trigger square-wave + fire-mode cycle
partial void StepDemo( float dt, ref bool active )
{
if ( !Demo ) return;
active = true;
// Cycle the fire mode every few seconds, then drive a synthetic trigger square-wave (held ~1s,
// released ~0.4s) through the real fire control so each mode visibly fires differently.
_modeT -= dt;
if ( _modeT <= 0f ) { _modeT = 3.0f; CycleFireMode(); }
_fire.Mode = FireMode; _fire.BurstCount = BurstCount; _fire.Rpm = RoundsPerMinute;
_triggerT += dt;
bool trigger = _triggerT % 1.4f < 1.0f;
if ( !_ammo.Reloading )
{
if ( _ammo.Mag <= 0 ) _ammo.Reload();
else if ( _fire.Tick( trigger, dt ) )
{
_ammo.Fire(); _crosshair.Fire();
if ( _demoRng.NextSingle() < 0.5f ) _hitmarker.Pop( _demoRng.NextSingle() < 0.15f );
}
}
FpsDemo.Health( _health, ref _hpT, dt );
FpsDemo.Stamina( _stamina, ref _stT, dt );
FpsDemo.Killfeed( _killfeed, ref _kfT, ref _kfPick, dt );
FpsDemo.Xp( _xp, ref _xpT, ref _xpPick, dt );
if ( ShowScoreboard ) FpsDemo.Scoreboard( _scoreboard, ref _sbT, dt );
}
}
using System;
using Sandbox;
namespace Goo.FpsUI;
// A drop-in shootable target for the FPS pack. Attach to any GameObject that has a Collider; the HUD's
// hitscan damages it through the engine's Component.IDamageable interface (the same path TriggerHurt uses).
// Exposes Health / IsDead / Name so FpsHud can drive the hitmarker, killfeed, and XP on a hit or kill.
// Engine-side component, not a Goo panel.
[Title( "FPS Target" ), Category( "FPS UI" ), Icon( "my_location" )]
public sealed class FpsTarget : Component, Component.IDamageable
{
[Property] public string Name { get; set; } = "Target"; // shown as the victim in the killfeed
[Property, Range( 1f, 1000f )] public float MaxHealth { get; set; } = 100f; // health restored on enable / respawn
[Property] public bool RespawnOnDeath { get; set; } = true; // refill health a moment after dying
[Property, Range( 0.5f, 10f )] public float RespawnDelay { get; set; } = 3f; // seconds dead before respawning
public float Health { get; private set; }
public bool IsDead => Health <= 0f;
float _respawnAt;
protected override void OnEnabled() => Health = MaxHealth;
// Engine damage entry point: TriggerHurt volumes and the HUD's hitscan both call this.
public void OnDamage( in DamageInfo damage )
{
if ( IsDead ) return; // already down, ignore extra hits
Health = Math.Max( 0f, Health - damage.Damage );
if ( IsDead && RespawnOnDeath ) _respawnAt = Time.Now + RespawnDelay;
}
protected override void OnUpdate()
{
if ( IsDead && RespawnOnDeath && Time.Now >= _respawnAt ) Health = MaxHealth;
}
}
using System;
namespace Goo.FpsUI;
// Compass logic: a 0..360 heading plus the shortest signed angular delta the view uses to
// place cardinal ticks. Engine-free.
public sealed class CompassModel
{
public float Heading { get; private set; } // current yaw, normalized 0..360
public void SetHeading( float yawDeg ) // set heading from game code (any range)
=> Heading = ((yawDeg % 360f) + 360f) % 360f;
// Signed delta from `from` to `to`, in (-180, 180]. Used to position ticks around center.
public static float ShortestDelta( float from, float to ) => ((to - from + 540f) % 360f) - 180f;
public bool Tick( float dt ) => false; // heading changes drive rebuilds, not the damper
}
using System;
using Goo;
using Sandbox;
using Sandbox.UI;
using PanelTransform = Goo.PanelTransform; // rule 18: ambiguous with Sandbox.UI otherwise
namespace Goo.FpsUI;
// Shared, stateless view helpers. Each returns a fresh Container (never cache a Container).
static class Parts
{
public enum Corner { TopLeft, TopCenter, TopRight, BottomLeft, BottomRight, Center }
// Absolute fill spanning `frac` (0..1) of its Relative parent's width, full height.
public static Container FillRect( string key, float frac, Color color, float radius ) => new()
{
Key = key, Position = PositionMode.Absolute, Top = 0, Left = 0,
Height = Length.Percent( 100 ),
Width = Length.Percent( Math.Clamp( frac, 0f, 1f ) * 100f ),
BorderRadius = radius, BackgroundColor = color,
};
// Absolute full-cover tint at a given opacity (flashes, warnings).
public static Container Overlay( string key, Color color, float opacity, float radius ) => new()
{
Key = key, Position = PositionMode.Absolute, Top = 0, Left = 0,
Width = Length.Percent( 100 ), Height = Length.Percent( 100 ),
BorderRadius = radius, BackgroundColor = color, Opacity = opacity,
};
// Pin `child` to a screen corner/center inside a Relative, full-size root. `m` is the edge inset.
// Container properties are init-only (record struct), so we use `with` expressions per corner.
public static Container Anchor( string key, Corner c, float m, Container child )
{
var box = new Container
{
Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
Children = { child },
};
return c switch
{
Corner.TopLeft => box with { Top = m, Left = m },
Corner.TopRight => box with { Top = m, Right = m },
Corner.BottomLeft => box with { Bottom = m, Left = m },
Corner.BottomRight => box with { Bottom = m, Right = m },
Corner.TopCenter => box with
{
Top = m, Left = Length.Percent( 50 ),
Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( 0 ) ?? default ),
},
_ => box with // Center
{
Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( -50 ) ?? default ),
},
};
}
// Pin `child` horizontally centered, offset `dy` px from the screen center (positive = down, negative = up).
public static Container CenterOffset( string key, float dy, Container child ) => new()
{
Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Px.Of( dy ) ),
Children = { child },
};
// Pin `child`'s top-left corner at a pixel offset from the screen center (positive dx = right, positive dy = down).
public static Container Offset( string key, float dx, float dy, Container child ) => new()
{
Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
Transform = PanelTransform.Translate( Px.Of( dx ), Px.Of( dy ) ),
Children = { child },
};
// Semi-opaque rounded backing behind a HUD readout, so text/bars stay legible over the world.
// Wraps one child and sizes to it, all card styling comes from the theme.
public static Container Panel( string key, FpsTheme t, Container child ) => new()
{
Key = key, Position = PositionMode.Relative,
Padding = t.PanelPad, BorderRadius = t.PanelRadius,
BackgroundColor = t.BackingBg,
Children = { child },
};
// A full-size, pointer-through, Relative root used by every standalone widget and the FpsHud.
public static Container Root( string key ) => new()
{
Key = key, Width = Length.Percent( 100 ), Height = Length.Percent( 100 ),
Position = PositionMode.Relative, PointerEvents = PointerEvents.None,
};
}
using System;
using Goo;
using Sandbox;
using Sandbox.UI;
namespace Goo.FpsUI;
// Stateless presenter: the secondary (stamina/armor) bar.
static class StaminaView
{
public const float TrackW = 320f, TrackH = 10f;
public static Container Build( StaminaModel m, FpsTheme t )
{
float frac = m.ShownFraction;
Color fill = Color.Lerp( t.Warn, t.Good, frac );
var track = new Container
{
Key = "stTrack", Position = PositionMode.Relative,
Width = TrackW, Height = TrackH, BackgroundColor = t.TrackBg,
BorderRadius = t.Radius, Overflow = OverflowMode.Hidden,
Children = { Parts.FillRect( "fill", frac, fill, t.Radius ) },
};
if ( m.Flash > 0.001f ) track.Children.Add( Parts.Overlay( "flash", Color.White, m.Flash * 0.8f, t.Radius ) );
return track;
}
}
// Standalone stamina bar. Call Sprinting(bool) from your movement code each frame.
public sealed partial class StaminaWidget : GooPanel<Container>
{
[Property, Range( 1f, 1000f )] public float MaxStamina { get; set; } = 100f; // full-bar value
[Property, Range( 0.05f, 2f )] public float DrainRate { get; set; } = 0.55f; // fraction of max drained per second while sprinting
[Property] public PlayerController? Player { get; set; } // player to gate sprint on when stamina is empty (null = skip gating)
readonly StaminaModel _m = new();
readonly FpsTheme _t = new();
bool _booted;
bool _sprintBlocked;
float _cachedRunSpeed;
void Boot() { _m.MaxStamina = MaxStamina; _m.Reset(); _booted = true; }
public void Sprinting( bool on ) => _m.SetSprinting( on ); // toggle sprint drain
// Demo-only seam: implemented in FpsDemo.cs, compiles out when that file is deleted.
partial void StepDemo( float dt, ref bool active );
protected override bool Tick( float dt )
{
if ( !_booted ) Boot();
_m.DrainRate = DrainRate; // live-tunable
bool demo = false;
StepDemo( dt, ref demo );
if ( !demo )
{
Sprinting( Sandbox.Input.Down( "run" ) );
FpsInput.ApplySprintGate( Player, _m.Stamina, ref _sprintBlocked, ref _cachedRunSpeed );
}
bool moving = _m.Tick( dt );
return demo || moving;
}
protected override Container Build()
{
if ( !_booted ) Boot();
var root = Parts.Root( "fpsStamina" );
// Sits just above where the health bar would be (health height + gap).
root.Children.Add( Parts.Anchor( "a", Parts.Corner.BottomLeft, _t.Margin + HealthView.TrackH + 10f, Parts.Panel( "bg", _t, StaminaView.Build( _m, _t ) ) ) );
return root;
}
}
using System;
using Sandbox;
using Sandbox.UI;
namespace Goo.Internal;
internal sealed class StatefulShapePanel : Panel, IStatefulEventHost
{
internal Action<MousePanelEvent>? _onClick;
internal Action<MousePanelEvent>? _onRightClick;
internal Action<MousePanelEvent>? _onMiddleClick;
internal Action<MousePanelEvent>? _onMouseEnter;
internal Action<MousePanelEvent>? _onMouseLeave;
internal Action<MousePanelEvent>? _onMouseDown;
internal Action<MousePanelEvent>? _onMouseUp;
internal Action<MousePanelEvent>? _onMouseMove;
internal bool _userSetPointerEvents;
internal Action? _requestRebuild;
public Action? RequestRebuild { set => _requestRebuild = value; }
public void ApplyEvents(in BlobEvents events)
{
_onClick = events.OnClick;
_onRightClick = events.OnRightClick;
_onMiddleClick = events.OnMiddleClick;
_onMouseEnter = events.OnMouseEnter;
_onMouseLeave = events.OnMouseLeave;
_onMouseDown = events.OnMouseDown;
_onMouseUp = events.OnMouseUp;
_onMouseMove = events.OnMouseMove;
}
public bool HasEventHandlers =>
_onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
_onMouseDown != null || _onMouseUp != null || _onMouseMove != null;
public bool UserSetPointerEvents
{
get => _userSetPointerEvents;
set => _userSetPointerEvents = value;
}
// Apply a baked alpha texture as BackgroundImage. The shape color comes from
// BackgroundColor flowing through the engine's BgTint multiplier; this method
// is only responsible for the alpha mask.
public void ApplyShape(BlobKind kind, in ShapeParams shape)
{
Style.BackgroundImage = ShapeTextureCache.GetOrBake(kind, in shape);
Style.BackgroundSizeX = Length.Percent(100);
Style.BackgroundSizeY = Length.Percent(100);
}
public void ApplyPolygon(Vector2[] points)
{
Style.BackgroundImage = ShapeTextureCache.GetOrBakePolygon(points);
Style.BackgroundSizeX = Length.Percent(100);
Style.BackgroundSizeY = Length.Percent(100);
}
protected override void OnClick(MousePanelEvent e) { base.OnClick(e); EventDispatch.Fire(_onClick, e, _requestRebuild); }
protected override void OnRightClick(MousePanelEvent e) { base.OnRightClick(e); EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
protected override void OnMouseOver(MousePanelEvent e) { base.OnMouseOver(e); EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
protected override void OnMouseOut(MousePanelEvent e) { base.OnMouseOut(e); EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
protected override void OnMouseDown(MousePanelEvent e) { base.OnMouseDown(e); EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
protected override void OnMouseUp(MousePanelEvent e) { base.OnMouseUp(e); EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
protected override void OnMouseMove(MousePanelEvent e) { base.OnMouseMove(e); EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
using System;
using Sandbox.UI;
namespace Goo.Internal;
internal sealed class StatefulSvgPanel : Sandbox.UI.SvgPanel, IStatefulEventHost
{
internal Action<MousePanelEvent>? _onClick;
internal Action<MousePanelEvent>? _onRightClick;
internal Action<MousePanelEvent>? _onMiddleClick;
internal Action<MousePanelEvent>? _onMouseEnter;
internal Action<MousePanelEvent>? _onMouseLeave;
internal Action<MousePanelEvent>? _onMouseDown;
internal Action<MousePanelEvent>? _onMouseUp;
internal Action<MousePanelEvent>? _onMouseMove;
internal bool _userSetPointerEvents;
internal Action? _requestRebuild;
public Action? RequestRebuild { set => _requestRebuild = value; }
public void ApplyEvents(in BlobEvents events)
{
_onClick = events.OnClick;
_onRightClick = events.OnRightClick;
_onMiddleClick = events.OnMiddleClick;
_onMouseEnter = events.OnMouseEnter;
_onMouseLeave = events.OnMouseLeave;
_onMouseDown = events.OnMouseDown;
_onMouseUp = events.OnMouseUp;
_onMouseMove = events.OnMouseMove;
}
public bool HasEventHandlers =>
_onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
_onMouseDown != null || _onMouseUp != null || _onMouseMove != null;
public bool UserSetPointerEvents
{
get => _userSetPointerEvents;
set => _userSetPointerEvents = value;
}
protected override void OnClick(MousePanelEvent e) { base.OnClick(e); EventDispatch.Fire(_onClick, e, _requestRebuild); }
protected override void OnRightClick(MousePanelEvent e) { base.OnRightClick(e); EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
protected override void OnMouseOver(MousePanelEvent e) { base.OnMouseOver(e); EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
protected override void OnMouseOut(MousePanelEvent e) { base.OnMouseOut(e); EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
protected override void OnMouseDown(MousePanelEvent e) { base.OnMouseDown(e); EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
protected override void OnMouseUp(MousePanelEvent e) { base.OnMouseUp(e); EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
protected override void OnMouseMove(MousePanelEvent e) { base.OnMouseMove(e); EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
namespace LobbySystem;
/// <summary>
/// Auto-hosts a lobby so Steam friends can join, and keeps one networked pawn per connection plus optional
/// bots by cloning <see cref="PlayerPrefab"/>. The pawn only has to implement <see cref="ILobbyAgent"/>.
/// Spawning is de-duped and runs in OnUpdate so a join can't fire mid-enumeration.
/// </summary>
public sealed class LobbyNetworkManager : Component, Component.INetworkListener
{
[Property] public GameObject PlayerPrefab { get; set; }
[Property] public int BotCount { get; set; } = 1;
/// <summary>When true, bots only exist during an active round.</summary>
[Property] public bool BotsOnlyDuringRound { get; set; } = true;
[Property] public Color BotTint { get; set; } = new Color( 1f, 0.35f, 0.3f );
// Lobby spawn ring, used before a round map loads.
readonly Vector3[] _spawns =
{
new Vector3( 0f, -300f, 40f ), new Vector3( 300f, 0f, 40f ),
new Vector3( 0f, 300f, 40f ), new Vector3( -300f, 0f, 40f ),
new Vector3( 250f, 250f, 40f ), new Vector3( -250f, -250f, 40f ),
};
int _spawnIndex;
readonly Dictionary<Guid, GameObject> _pawns = new();
readonly List<GameObject> _bots = new();
bool _reconcileNow;
TimeUntil _nextReconcile;
TimeUntil _nextSweep;
protected override async Task OnLoad()
{
// When joining a friend the engine is mid-connect and IsActive is briefly false, so poll for a
// moment before hosting. Otherwise a joiner would spin up its own solo lobby.
if ( Networking.IsActive ) return;
for ( int i = 0; i < 6 && !Networking.IsActive; i++ )
await Task.DelayRealtimeSeconds( 0.1f );
if ( !Networking.IsActive )
Networking.CreateLobby( new() );
}
void INetworkListener.OnActive( Connection channel ) => _reconcileNow = true;
protected override void OnUpdate()
{
if ( !Networking.IsHost || PlayerPrefab is null ) return;
if ( !_reconcileNow && _nextReconcile > 0f ) return;
_reconcileNow = false;
_nextReconcile = 0.25f;
try
{
bool wantBots = !BotsOnlyDuringRound || (LobbyDirector.Current?.State == LobbyState.Active);
ReconcileBots( wantBots ? Math.Max( 0, BotCount ) : 0 );
foreach ( var conn in Connection.All.ToList() )
{
if ( conn is null || !conn.IsActive ) continue;
if ( _pawns.TryGetValue( conn.Id, out var pawn ) && pawn.IsValid() ) continue;
var id = conn.Id;
_pawns[id] = FindConnectionPawn( id ) ?? SpawnPawn( false, conn.DisplayName, conn );
}
Sweep();
}
catch
{
// Connection or scene list changed during the pass; retry next frame.
}
}
void ReconcileBots( int target )
{
_bots.RemoveAll( b => !b.IsValid() );
while ( _bots.Count > target )
{
var b = _bots[_bots.Count - 1];
_bots.RemoveAt( _bots.Count - 1 );
if ( b.IsValid() ) b.Destroy();
}
while ( _bots.Count < target )
_bots.Add( SpawnPawn( true, "Bot", null ) );
}
GameObject FindConnectionPawn( Guid id )
{
foreach ( var a in Scene.GetAllComponents<ILobbyAgent>() )
{
if ( !a.IsValid() || a.IsBot ) continue;
if ( a is Component c && c.Network.OwnerId == id ) return c.GameObject;
}
return null;
}
void Sweep()
{
if ( _nextSweep > 0f ) return;
_nextSweep = 1f;
foreach ( var key in _pawns.Where( kv => !kv.Value.IsValid() ).Select( kv => kv.Key ).ToList() )
_pawns.Remove( key );
}
GameObject SpawnPawn( bool isBot, string displayName, Connection owner )
{
var go = PlayerPrefab.Clone( NextSpawn() );
go.Name = isBot ? "Bot" : $"Player - {displayName}";
go.Enabled = true;
var agent = go.Components.Get<ILobbyAgent>() ?? go.Components.GetInChildren<ILobbyAgent>();
agent?.InitAgent( isBot, displayName );
if ( isBot )
{
var rend = go.Components.GetInChildren<SkinnedModelRenderer>();
if ( rend is not null ) rend.Tint = BotTint;
}
if ( owner is not null ) go.NetworkSpawn( owner );
else go.NetworkSpawn();
return go;
}
Vector3 NextSpawn()
{
int idx = _spawnIndex++;
var dir = LobbyDirector.Current;
if ( dir is not null && dir.UseRoundMap && dir.MapReady )
return dir.RoundSpawnPoint( idx );
return _spawns[idx % _spawns.Length];
}
}
namespace LobbySystem;
/// <summary>Lifecycle of the lobby: Lobby, then Active, then Ended before looping back.</summary>
public enum LobbyState
{
/// <summary>Free roam before and after a round. The mode button works here.</summary>
Lobby,
/// <summary>A round is running.</summary>
Active,
/// <summary>Round finished; results show before returning to the lobby.</summary>
Ended
}
namespace LobbySystem;
/// <summary>
/// In-world button that opens the mode menu for the host, or a local suggestion menu for a client. It needs
/// a ModelRenderer to be visible and is hidden while a round is live. When the local player is within
/// <see cref="UseRange"/> and presses Use, the menu opens.
/// </summary>
public sealed class LobbyModeButton : Component
{
[Property] public float UseRange { get; set; } = 130f;
[Property] public bool GlowWhenInRange { get; set; } = true;
[Property] public Color IdleTint { get; set; } = new Color( 0.85f, 0.4f, 0.15f );
[Property] public Color ActiveTint { get; set; } = new Color( 1f, 0.85f, 0.3f );
ModelRenderer _renderer;
ILobbyAgent _me;
protected override void OnStart()
{
_renderer = Components.Get<ModelRenderer>() ?? Components.GetInChildren<ModelRenderer>();
if ( _renderer is not null ) _renderer.Tint = IdleTint;
}
protected override void OnUpdate()
{
var dir = LobbyDirector.Current;
bool inLobby = dir is null || !dir.RoundLive;
if ( _renderer is not null && _renderer.Enabled != inLobby )
_renderer.Enabled = inLobby;
if ( !inLobby ) return;
var me = LocalPlayer();
bool inRange = me is not null && WorldPosition.Distance( me.WorldPosition ) <= UseRange;
if ( GlowWhenInRange && _renderer is not null )
_renderer.Tint = inRange ? ActiveTint : IdleTint;
if ( inRange && Input.Pressed( "Use" ) )
dir?.RequestModeMenu();
}
ILobbyAgent LocalPlayer()
{
if ( _me is not null && _me.IsValid() && !_me.IsBot && !_me.IsProxy ) return _me;
try { _me = Scene.GetAllComponents<ILobbyAgent>().FirstOrDefault( c => c.IsValid() && !c.IsBot && !c.IsProxy ); }
catch { _me = null; }
return _me;
}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "MC Clouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "trend" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "trend.mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-16T17:04:05.4666731Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.124.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.124.0")]using Sandbox;
using Sandbox.UI;
using System;
namespace SbTween;
public static class LightExtensions
{
public static BaseTween TweenLightColor( this Light Light, Color target, float duration )
{
Color start = Light.LightColor;
var tween = new BaseTween( duration );
tween.Target = Light.GameObject;
return TweenManager.Instance.AddTween( tween
.OnStart( () => start = Light.LightColor )
.OnUpdate( p => Light.LightColor = Color.Lerp( start, target, p ) ) );
}
public static BaseTween TweenRadius( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Radius;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Radius )
.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenConeOuter( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.ConeOuter;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.ConeOuter )
.OnUpdate( p => light.ConeOuter = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenInnerCone( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.ConeInner;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.ConeInner )
.OnUpdate( p => light.ConeInner = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenAttenuation( this PointLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Attenuation;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Attenuation )
.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenRadius( this PointLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Radius;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Radius )
.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
}
public static BaseTween TweenAttenuation( this SpotLight light, float target, float duration )
{
if ( !light.IsValid() ) return null;
float start = light.Attenuation;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnStart( () => start = light.Attenuation )
.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
}
//FLICKERING LIGHT
public static BaseTween TweenFlickerLight( this PointLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
} ) );
}
public static BaseTween TweenFlickerLight( this SpotLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
} ) );
}
//FLICKERING Color
public static BaseTween TweenFlickerColor( this PointLight light, Color colorA, Color colorB, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.LightColor = Color.Lerp( colorA, colorB, t );
} ) );
}
public static BaseTween TweenFlickerColor( this SpotLight light, Color colorA, Color colorB, float duration, float speed = 10f )
{
if ( !light.IsValid() ) return null;
float time = 0f;
return TweenManager.Instance.AddTween( new BaseTween( duration )
.OnUpdate( _ =>
{
time += Time.Delta * speed;
float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
float t = (noise + 1f) * 0.5f;
light.LightColor = Color.Lerp( colorA, colorB, t );
} ) );
}
}
using Sandbox;
using System;
namespace SbTween;
public static class AudioExtensions
{
public static BaseTween TweenVolume( this SoundPointComponent sound, float targetVolume, float duration )
{
float startVolume = sound.Volume;
var tween = new BaseTween( duration );
tween.Target = sound.GameObject;
return TweenManager.Instance.AddTween( tween
.OnStart( () => startVolume = sound.Volume )
.OnUpdate( p =>
{
if ( !sound.IsValid() ) return;
sound.Volume = MathX.Lerp( startVolume, targetVolume, p );
} )
);
}
public static BaseTween TweenPitch( this SoundPointComponent sound, float targetPitch, float duration )
{
float startPitch = sound.Pitch;
var tween = new BaseTween( duration );
tween.Target = sound.GameObject;
return TweenManager.Instance.AddTween( tween
.OnStart( () => startPitch = sound.Pitch )
.OnUpdate( p =>
{
if ( !sound.IsValid() ) return;
sound.Pitch = MathX.Lerp( startPitch, targetPitch, p );
} )
);
}
}
using Sandbox;
using System;
namespace SbTween;
public static class MathTweenExtensions
{
public static BaseTween TweenInCircle( this GameObject obj, float duration, Vector3 axis, float range, float speed, bool snapping = false )
{
Vector3 centerPos = obj.WorldPosition;
var tween = new BaseTween( duration );
tween.Target = obj;
Vector3 normal = axis.Normal;
Vector3 v1 = Vector3.Cross( normal, MathF.Abs( normal.z ) < 0.9f ? Vector3.Up : Vector3.Forward ).Normal;
Vector3 v2 = Vector3.Cross( normal, v1 ).Normal;
return TweenManager.Instance.AddTween( tween
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
float angleDegrees = p * 360f * speed;
float angleRadians = angleDegrees * (MathF.PI / 180f);
float cos = MathF.Cos( angleRadians ) * range;
float sin = MathF.Sin( angleRadians ) * range;
Vector3 rotatedOffset = (v1 * cos) + (v2 * sin);
obj.WorldPosition = centerPos + rotatedOffset;
} )
);
}
public static BaseTween TweenSpiral( this GameObject obj, float duration, Vector3 axis, float speed, float frequency )
{
Vector3 startPos = obj.WorldPosition;
var tween = new BaseTween( duration );
tween.Target = obj;
return TweenManager.Instance.AddTween( tween
.OnStart( () => startPos = obj.WorldPosition )
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
float angle = p * MathF.PI * 2f * frequency;
float currentRadius = p * speed;
float x = MathF.Cos( angle ) * currentRadius;
float y = MathF.Sin( angle ) * currentRadius;
Vector3 axisOffset = axis * p;
Vector3 circleOffset = new Vector3( x, y, 0 );
obj.WorldPosition = startPos + axisOffset + circleOffset;
} )
);
}
public static BaseTween TweenPunchFloat( this GameObject obj, float v, float amplitude, float duration, int vibrations = 5, float elasticity = 1f, Action<float> setter = null )
{
var tween = new BaseTween( duration );
tween.Target = obj;
return TweenManager.Instance.AddTween( tween
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
if ( p >= 1.0f )
{
setter?.Invoke( v );
return;
}
float decay = MathF.Pow( 1f - p, elasticity * 3f );
float omega = vibrations * MathF.PI * 2f;
float oscillation = MathF.Sin( p * omega );
float currentOffset = amplitude * oscillation * decay;
setter?.Invoke( v + currentOffset );
} )
.OnComplete( () => setter?.Invoke( v ) )
);
}
public static BaseTween TweenShakeFloat( this GameObject obj, float baseline, float strength, float duration, Action<float> setter = null )
{
var tween = new BaseTween( duration );
tween.Target = obj;
return TweenManager.Instance.AddTween( tween
.OnUpdate( p =>
{
if ( !obj.IsValid() ) return;
if ( p >= 1.0f )
{
setter?.Invoke( baseline );
return;
}
float currentStrength = strength * (1.0f - p);
float randomOffset = Game.Random.Float( -currentStrength, currentStrength );
setter?.Invoke( baseline + randomOffset );
} )
.OnComplete( () => setter?.Invoke( baseline ) )
);
}
}
namespace Skateboard.Entities;
public sealed class GrindPathNodeEntity : Component
{
}
#nullable enable
using Sandbox;
public abstract class Item
{
public Scene? Scene {get;set;} = null;
public GameObject? GameObject {get;set;} = null;
public string Name {get;set;} = "empty";
public abstract Item Init();
public abstract void Act();
public abstract void Drop();
}using Sandbox;
public class SCP_173_SFX : Component
{
[Property] public CharacterController Character_Controller {get;set;}
[Property] public SoundPointComponent Rattle {get;set;}
protected override void OnUpdate()
{ if (IsProxy) return;
if (Character_Controller.IsOnGround && Character_Controller.Velocity.LengthSquared > 0
&& !GameObject.Tags.Has("seen")) {Rattle.StartSound(); return;} Rattle.StopSound();
}
}namespace Sandbox.UiPro;
public enum HorizontalAlignment
{
Left,
Center,
Right
}
public enum VerticalAlignment
{
Top,
Center,
Bottom
}
[Title( "Text Node - UI Pro" ), Category( "UI Pro" ), Icon( "text_fields" )]
public class TextNode : NodeComponent
{
[Property, InlineEditor, Group( "Layout Settings" ), Order( -999 )] public override NodeStyle Style { get; set; } = GetDefaultStyle();
[Property, Group("Text Settings")] public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Center;
[Property, Group( "Text Settings" )] public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Center;
[Property, InlineEditor, Group( "Text Settings" )] public TextRendering.Scope TextScope { get; set; } = TextRendering.Scope.Default;
private static NodeStyle GetDefaultStyle()
{
return new NodeStyle()
{
Anchor = NodePoint.CenterMiddle,
Pivot = NodePoint.CenterMiddle,
Offset = Vector2.Zero,
Size = new Vector2( 100, 100 ),
ChildPadding = Vector2.Zero,
ClipChildren = false,
StretchHorizontal = false,
StretchVertical = false,
CornerRadius = 0,
BorderWidth = 0,
BorderColor = Color.Black,
Texture = Texture.White,
Tint = Color.White,
UvScale = Vector2.One,
UvOffset = Vector2.Zero
};
}
protected override void UpdateStyle(float scaleFactor)
{
Style.BorderWidth = 0; // for debugging
Style.BorderColor = Color.White;
TextRendering.Scope scope = TextScope;
scope.FontSize *= scaleFactor;
Texture texture = TextRendering.GetOrCreateTexture( scope );
Style.Texture = texture;
Vector2 scale = (Layout.Outer.Size * scaleFactor) / texture.Size;
Style.UvScale = scale;
float alignX = HorizontalAlignment switch
{
HorizontalAlignment.Left => 0f,
HorizontalAlignment.Center => 0.5f,
HorizontalAlignment.Right => 1f,
_ => 0.5f,
};
float alignY = VerticalAlignment switch
{
VerticalAlignment.Top => 0f,
VerticalAlignment.Center => 0.5f,
VerticalAlignment.Bottom => 1f,
_ => 0.5f,
};
float offsetX = alignX * (1f / scale.x - 1f);
float offsetY = alignY * (1f / scale.y - 1f);
Style.UvOffset = new Vector2( offsetX, offsetY );
}
}
using Sandbox.UiPro;
namespace Sandbox;
// Hooks up the Button's OnClick event and responds
// by updating the TextNode
public class ExampleButtonController : Component
{
[Property] public Button MyButton { get; set; }
[Property] public TextNode MyText { get; set; }
[Property, ReadOnly] public int ClickCount { get; set; } = 0;
protected override void OnStart()
{
if ( !MyButton.IsValid() ) return;
MyButton.OnClick = OnButtonClicked;
}
private void OnButtonClicked()
{
ClickCount++;
if ( !MyText.IsValid() ) return;
TextRendering.Scope scope = MyText.TextScope;
scope.Text = $"Clicked {ClickCount} Times";
MyText.TextScope = scope;
}
}
using System;
namespace Sandbox.UiPro;
public enum NodePoint
{
TopLeft, TopMiddle, TopRight,
CenterLeft, CenterMiddle, CenterRight,
BottomLeft, BottomMiddle, BottomRight,
}
public class NodeStyle
{
[Property] public NodePoint Anchor { get; set; }
[Property] public NodePoint Pivot { get; set; }
[Property] public Vector2 Offset { get; set; }
[Property] public Vector2 Size { get; set; }
[Property] public Vector4 ChildPadding { get; set; }
[Property] public bool ClipChildren { get; set; }
[Property] public bool StretchHorizontal { get; set; }
[Property] public bool StretchVertical { get; set; }
[Property, Hide] public float CornerRadius { get; set; }
[Property, Hide] public float BorderWidth { get; set; }
[Property, Hide] public Color BorderColor { get; set; }
[Property, Hide] public Texture Texture { get; set; }
[Property, Hide] public Color Tint { get; set; }
[Property, Hide] public Vector2 UvScale { get; set; }
[Property, Hide] public Vector2 UvOffset { get; set; }
}
public class NodeLayout
{
public Rect Outer { get; private set; }
public Rect Inner { get; private set; }
public Rect ClipRect { get; private set; }
public float ClipRadius { get; private set; }
public Rect ChildClipRect { get; private set; }
public float ChildClipRadius { get; private set; }
public static NodeLayout GetRootLayout( Vector2 size )
{
Rect rootRect = new Rect( Vector2.Zero, size );
NodeLayout layout = new NodeLayout()
{
Outer = rootRect,
Inner = rootRect,
ClipRect = rootRect,
ClipRadius = 0,
ChildClipRect = rootRect,
ChildClipRadius = 0
};
return layout;
}
public void Compute( NodeLayout parentLayout, NodeStyle style )
{
Vector2 anchor = AnchorFraction( style.Anchor );
Vector2 pivot = AnchorFraction( style.Pivot );
float width = style.StretchHorizontal ? parentLayout.Inner.Size.x : style.Size.x;
float height = style.StretchVertical ? parentLayout.Inner.Size.y : style.Size.y;
float x = style.StretchHorizontal
? parentLayout.Inner.Position.x + style.Offset.x
: parentLayout.Inner.Position.x + anchor.x * parentLayout.Inner.Size.x - pivot.x * width + style.Offset.x;
float y = style.StretchVertical
? parentLayout.Inner.Position.y + style.Offset.y
: parentLayout.Inner.Position.y + anchor.y * parentLayout.Inner.Size.y - pivot.y * height + style.Offset.y;
Outer = new Rect( new Vector2( x, y ), new Vector2( width, height ) );
float innerW = Math.Max( 0f, width - style.ChildPadding.x - style.ChildPadding.z );
float innerH = Math.Max( 0f, height - style.ChildPadding.y - style.ChildPadding.w );
Inner = new Rect( new Vector2( x + style.ChildPadding.x, y + style.ChildPadding.y ), new Vector2( innerW, innerH ) );
ClipRect = parentLayout.ChildClipRect;
ClipRadius = parentLayout.ChildClipRadius;
if ( style.ClipChildren )
{
float bw = Math.Max( 0f, style.BorderWidth );
float clipW = Math.Max( 0f, Outer.Size.x - bw * 2f );
float clipH = Math.Max( 0f, Outer.Size.y - bw * 2f );
ChildClipRect = new Rect( new Vector2( Outer.Position.x + bw, Outer.Position.y + bw ), new Vector2( clipW, clipH ) );
ChildClipRadius = Math.Max( 0f, style.CornerRadius - bw );
}
else
{
ChildClipRect = parentLayout.ChildClipRect;
ChildClipRadius = parentLayout.ChildClipRadius;
}
}
private static Vector2 AnchorFraction( NodePoint point )
{
float fx = point switch
{
NodePoint.TopLeft or NodePoint.CenterLeft or NodePoint.BottomLeft => 0f,
NodePoint.TopMiddle or NodePoint.CenterMiddle or NodePoint.BottomMiddle => 0.5f,
_ => 1f,
};
float fy = point switch
{
NodePoint.TopLeft or NodePoint.TopMiddle or NodePoint.TopRight => 0f,
NodePoint.CenterLeft or NodePoint.CenterMiddle or NodePoint.CenterRight => 0.5f,
_ => 1f,
};
return new Vector2( fx, fy );
}
}
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dreams.UltimateLightManager;
[Library( "UltimateLightManager" )]
[Title( "Ultimate Light Manager" )]
[Description( "Advanced light component with presets, runtime controls, grouping, and an integrated S&box editor workflow." )]
[Category( "Light" )]
[Icon( "tungsten" )]
public class UltimateLightManager : Component, Component.ExecuteInEditor
{
public enum LightTypeEnum
{
Point,
Spot
}
public enum LightPreset
{
Custom,
Candle,
Torch,
Neon,
Alarm,
BrokenLamp,
SciFi,
StreetLight
}
public static void SetGroupState( string groupName, bool isEnabled )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.SetEnabledState( isEnabled );
}
}
public static void SetGroupBrightness( string groupName, float brightness )
{
brightness = Math.Max( brightness, 0f );
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.SetBrightnessLevel( brightness );
}
}
public static void SetGroupColor( string groupName, Color color )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.SetLightColorValue( color );
}
}
public static void ApplyPresetToGroup( string groupName, LightPreset preset )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.ApplyPreset( preset );
}
}
public static void TriggerGroupFlash( string groupName, float duration = 0.15f, float brightnessMultiplier = 2f )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.TriggerFlash( duration, brightnessMultiplier );
}
}
public static void TriggerGroupAlarm( string groupName, float duration = 2f )
{
foreach ( var light in GetLightsInGroup( groupName ) )
{
light.TriggerAlarm( duration );
}
}
public static void SetPowerGridState( string powerGridTag, bool isPowered )
{
foreach ( var light in GetLightsInPowerGrid( powerGridTag ) )
{
light.SetPowered( isPowered );
}
}
private static IEnumerable<UltimateLightManager> GetLightsInGroup( string groupName )
{
return EnumerateAllLights().Where( light => string.Equals( light.LightGroup, groupName, StringComparison.OrdinalIgnoreCase ) );
}
private static IEnumerable<UltimateLightManager> GetLightsInPowerGrid( string powerGridTag )
{
return EnumerateAllLights().Where( light => string.Equals( light.PowerGridTag, powerGridTag, StringComparison.OrdinalIgnoreCase ) );
}
private static IEnumerable<UltimateLightManager> EnumerateAllLights()
{
var visitedScenes = new HashSet<Scene>();
var visitedLights = new HashSet<UltimateLightManager>();
foreach ( var scene in EnumerateCandidateScenes() )
{
if ( scene == null || !visitedScenes.Add( scene ) )
{
continue;
}
foreach ( var light in scene.GetAllComponents<UltimateLightManager>() )
{
if ( light != null && visitedLights.Add( light ) )
{
yield return light;
}
}
}
}
private static IEnumerable<Scene> EnumerateCandidateScenes()
{
if ( Game.ActiveScene != null )
{
yield return Game.ActiveScene;
}
foreach ( var scene in Scene.All )
{
if ( scene != null )
{
yield return scene;
}
}
}
[Property, Order( -1 ), Group( "Management" )]
public string LightGroup { get; set; } = "Default";
[Property, Group( "Management" )]
public string PowerGridTag { get; set; } = string.Empty;
[Property, Group( "Management" )]
public float StartDelay { get; set; } = 0.0f;
[Property, Group( "Management" )]
public bool AutoDesync { get; set; } = true;
[Property, Group( "Management" )]
public bool ShowDebugGizmos { get; set; } = false;
[Property, Group( "Management" )]
public bool ForceNetworkObjectMode { get; set; } = true;
[Property, Group( "General" ), Sync( SyncFlags.FromHost )]
public LightTypeEnum TargetLightType { get; set; } = LightTypeEnum.Point;
[Property, Group( "General" ), Sync( SyncFlags.FromHost )]
public bool IsEnabled { get; set; } = true;
[Property, Group( "General" ), Sync( SyncFlags.FromHost )]
public Color LightColor { get; set; } = Color.White;
[Property, Group( "General" ), Range( 0, 100 ), Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
public float Brightness { get; set; } = 1.0f;
[Property, Group( "General" ), Range( 0, 10 )]
public float VolumetricBoost { get; set; } = 1.0f;
[Property, Group( "General" )]
public bool CastShadows { get; set; } = true;
[Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
public LightPreset SelectedPreset { get; set; } = LightPreset.Custom;
[Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
public bool AutoApplyPreset { get; set; } = true;
[Property, Group( "Transitions" )]
public bool EnableFade { get; set; } = false;
[Property, Group( "Transitions" )]
public float FadeInDuration { get; set; } = 0.2f;
[Property, Group( "Transitions" )]
public float FadeOutDuration { get; set; } = 0.2f;
[Property, Group( "Audio" )]
public SoundEvent AmbientSound { get; set; }
[Property, Group( "Audio" )]
public SoundEvent ToggleOnSound { get; set; }
[Property, Group( "Audio" )]
public SoundEvent ToggleOffSound { get; set; }
[Property, Group( "Audio" )]
public bool ModulateVolumeWithLight { get; set; } = true;
[Property, Group( "Audio" )]
public bool ModulatePitchWithLight { get; set; } = false;
[Property, Group( "Audio" ), Range( 0, 5 )]
public float BaseVolume { get; set; } = 1.0f;
[Property, Group( "Audio" ), Range( 0.5f, 2f )]
public float MinPitch { get; set; } = 0.9f;
[Property, Group( "Audio" ), Range( 0.5f, 2f )]
public float MaxPitch { get; set; } = 1.1f;
[Property, Group( "Optimization" )]
public float MaxDistance { get; set; } = 2500.0f;
[Property, Group( "Optimization" )]
public float ShadowMaxDistance { get; set; } = 800.0f;
[Property, Group( "Optimization" )]
public bool EnableCulling { get; set; } = true;
[Property, Group( "Optimization" )]
public bool EnableAdaptiveUpdates { get; set; } = false;
[Property, Group( "Optimization" ), Range( 1, 120 )]
public float NearUpdateRate { get; set; } = 60.0f;
[Property, Group( "Optimization" ), Range( 1, 120 )]
public float FarUpdateRate { get; set; } = 12.0f;
[Property, Group( "Gameplay" )]
public float DefaultAlarmDuration { get; set; } = 2.0f;
[Property, Group( "Gameplay" ), Sync( SyncFlags.FromHost )]
public Color AlarmColor { get; set; } = new Color( 1.0f, 0.15f, 0.1f );
[Property, Group( "Gameplay" )]
public float AlarmStrobeSpeed { get; set; } = 8.0f;
[Property, Group( "Gameplay" )]
public float AlarmBrightnessMultiplier { get; set; } = 1.5f;
[Property, FeatureEnabled( "Fire & Candle" )]
public bool EnableFire { get; set; } = false;
[Property, Feature( "Fire & Candle" )]
public float FireSpeed { get; set; } = 12.0f;
[Property, Feature( "Fire & Candle" ), Range( 0, 1 )]
public float FireIntensity { get; set; } = 0.3f;
[Property, Feature( "Fire & Candle" ), Range( 0, 2 )]
public float FireChaos { get; set; } = 1.0f;
[Property, FeatureEnabled( "Horror Mode" )]
public bool EnableHorror { get; set; } = false;
[Property, Feature( "Horror Mode" )]
public float MinFlickerDelay { get; set; } = 0.05f;
[Property, Feature( "Horror Mode" )]
public float MaxFlickerDelay { get; set; } = 0.4f;
[Property, Feature( "Horror Mode" ), Range( 0, 1 )]
public float DamageSeverity { get; set; } = 0.8f;
[Property, Feature( "Horror Mode" )]
public SoundEvent SparkSound { get; set; }
[Property, FeatureEnabled( "Disco Mode" )]
public bool EnableDisco { get; set; } = false;
[Property, Feature( "Disco Mode" )]
public float DiscoSpeed { get; set; } = 20.0f;
[Property, Feature( "Disco Mode" ), Range( 0, 1 )]
public float DiscoSaturation { get; set; } = 1.0f;
[Property, Feature( "Disco Mode" ), Range( 0, 1 )]
public float DiscoValue { get; set; } = 1.0f;
[Property, FeatureEnabled( "Color Transition" )]
public bool EnableColorTransition { get; set; } = false;
[Property, Feature( "Color Transition" ), Sync( SyncFlags.FromHost )]
public Color SecondaryColor { get; set; } = new Color( 0.2f, 0.85f, 1.0f );
[Property, Feature( "Color Transition" )]
public float ColorTransitionSpeed { get; set; } = 1.0f;
[Property, FeatureEnabled( "Proximity Sensor" )]
public bool EnableSensor { get; set; } = false;
[Property, Feature( "Proximity Sensor" )]
public float SensorRange { get; set; } = 300.0f;
[Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
public float SensorMinBrightness { get; set; } = 0.0f;
[Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
public float SensorMaxBrightness { get; set; } = 1.0f;
[Property, Feature( "Proximity Sensor" ), Range( 1, 20 )]
public float SensorSmoothness { get; set; } = 5.0f;
[Property, Feature( "Proximity Sensor" )]
public bool InvertSensor { get; set; } = false;
[Property, FeatureEnabled( "Motion Sway" )]
public bool EnableSway { get; set; } = false;
[Property, Feature( "Motion Sway" )]
public float SwaySpeedPitch { get; set; } = 1.0f;
[Property, Feature( "Motion Sway" )]
public float SwayAmountPitch { get; set; } = 5.0f;
[Property, Feature( "Motion Sway" )]
public float SwaySpeedRoll { get; set; } = 0.7f;
[Property, Feature( "Motion Sway" )]
public float SwayAmountRoll { get; set; } = 3.0f;
[Property, FeatureEnabled( "Flicker Pattern" )]
public bool EnablePattern { get; set; } = false;
[Property, Feature( "Flicker Pattern" )]
public string Pattern { get; set; } = "mmnmmommommnonmmonqnmmo";
[Property, Feature( "Flicker Pattern" )]
public float PatternSpeed { get; set; } = 10.0f;
[Property, FeatureEnabled( "Pulse" )]
public bool EnablePulse { get; set; } = false;
[Property, Feature( "Pulse" )]
public float PulseSpeed { get; set; } = 1.0f;
[Property, Feature( "Pulse" ), Range( 0, 1 )]
public float PulseMin { get; set; } = 0.2f;
[Property, FeatureEnabled( "Strobe" )]
public bool EnableStrobe { get; set; } = false;
[Property, Feature( "Strobe" )]
public float StrobeSpeed { get; set; } = 10.0f;
[Property, Feature( "Strobe" ), Range( 0.1f, 0.9f )]
public float StrobeDutyCycle { get; set; } = 0.5f;
[Property, FeatureEnabled( "Kelvin" )]
public bool EnableKelvin { get; set; } = false;
[Property, Feature( "Kelvin" ), Range( 1000, 12000 )]
public int KelvinTemperature { get; set; } = 4500;
[Property, FeatureEnabled( "Power Surge" )]
public bool EnablePowerSurge { get; set; } = false;
[Property, Feature( "Power Surge" )]
public float SurgeMinInterval { get; set; } = 4.0f;
[Property, Feature( "Power Surge" )]
public float SurgeMaxInterval { get; set; } = 10.0f;
[Property, Feature( "Power Surge" )]
public float SurgeDuration { get; set; } = 0.15f;
[Property, Feature( "Power Surge" )]
public float SurgeBrightnessMultiplier { get; set; } = 1.8f;
public bool Powered => PoweredState;
public float ExternalBrightnessMultiplier => ExternalBrightnessMultiplierState;
public bool HasColorOverride => HasExternalColorOverrideState;
public bool AlarmActive => AlarmEndTimeState > RealTime.Now;
private PointLight _pointLight;
private SpotLight _spotLight;
private Light ActiveLight => TargetLightType == LightTypeEnum.Point ? (Light)_pointLight : _spotLight;
private int _lastKelvin = -1;
private Color _cachedKelvinColor = Color.White;
private Rotation _baseRotation;
private bool _isInitialized;
private bool _hasAppliedPreset;
private LightPreset _lastAppliedPreset = LightPreset.Custom;
private LightPreset _lastObservedPreset = LightPreset.Custom;
private bool _lastObservedAutoApplyPreset = true;
private LightTypeEnum _lastSyncedLightType = LightTypeEnum.Point;
private float _brokenMultiplier = 1.0f;
private float _nextFlicker;
private float _sensorWeightTarget = 1.0f;
private float _sensorWeightCurrent = 1.0f;
private float _randomTimeOffset;
private float _creationTime;
private float _lastUpdateTimestamp;
private float _enabledBlend = 1.0f;
private float _nextAdaptiveUpdateTime;
private float _nextViewerCameraRefreshTime;
private float _nextSurgeTime;
private float _surgeEndTime;
private bool _hasOutputState;
private bool _lastOutputEnabled;
private CameraComponent _cachedViewerCamera;
private SoundHandle _ambientSoundHandle;
[Sync( SyncFlags.FromHost )]
private bool PoweredState { get; set; } = true;
[Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
private float ExternalBrightnessMultiplierState { get; set; } = 1.0f;
[Sync( SyncFlags.FromHost )]
private bool HasExternalColorOverrideState { get; set; }
[Sync( SyncFlags.FromHost )]
private Color ExternalColorOverrideState { get; set; } = Color.White;
[Sync( SyncFlags.FromHost )]
private float FlashEndTimeState { get; set; }
[Sync( SyncFlags.FromHost )]
private float FlashBrightnessMultiplierState { get; set; } = 1.0f;
[Sync( SyncFlags.FromHost )]
private float AlarmEndTimeState { get; set; }
protected override void OnAwake()
{
EnsureNetworkMode();
}
protected override void OnStart()
{
EnsureNetworkMode();
_baseRotation = LocalRotation;
_creationTime = RealTime.Now;
_lastUpdateTimestamp = _creationTime;
_enabledBlend = IsEnabled ? 1.0f : 0.0f;
if ( AutoDesync )
{
_randomTimeOffset = Game.Random.Float( 0f, 100f );
}
SyncComponentsIfNeeded( force: true );
if ( AutoApplyPreset )
{
ApplyPresetInternal( SelectedPreset );
}
ScheduleNextSurge( _creationTime );
_lastObservedPreset = SelectedPreset;
_lastObservedAutoApplyPreset = AutoApplyPreset;
_isInitialized = true;
}
protected override void OnUpdate()
{
if ( !_isInitialized )
{
return;
}
SyncComponentsIfNeeded();
SyncPresetSelectionIfNeeded();
var light = ActiveLight;
if ( light == null )
{
return;
}
bool isPlaying = Game.IsPlaying;
float absoluteTime = RealTime.Now;
float effectTime = (isPlaying ? Time.Now : absoluteTime) + _randomTimeOffset;
float deltaTime = GetFrameDelta( absoluteTime );
if ( isPlaying && StartDelay > 0f && (absoluteTime - _creationTime) < StartDelay )
{
DisableOutput( light );
return;
}
var viewerCam = GetViewerCamera( absoluteTime );
float distSq = viewerCam != null ? WorldPosition.DistanceSquared( viewerCam.WorldPosition ) : 0f;
if ( ShouldSkipAdaptiveUpdate( isPlaying, viewerCam, distSq, absoluteTime ) )
{
UpdateAmbientSoundPosition();
return;
}
UpdateSensorWeight( viewerCam, distSq, deltaTime );
UpdateEnabledBlend( deltaTime );
UpdateSway( effectTime );
if ( isPlaying && EnableCulling && viewerCam != null && distSq > MaxDistance * MaxDistance )
{
DisableOutput( light );
return;
}
float fx = 1.0f;
fx *= EvaluatePulseAndStrobe( effectTime );
fx *= EvaluatePattern( effectTime );
fx *= EvaluateFire( effectTime );
fx *= EvaluateHorror( effectTime, isPlaying );
fx *= EvaluatePowerSurge( absoluteTime );
fx *= EvaluateAlarm( effectTime, absoluteTime );
if ( absoluteTime < FlashEndTimeState )
{
fx *= FlashBrightnessMultiplierState;
}
float finalBrightness = Brightness * fx * _sensorWeightCurrent * _enabledBlend * ExternalBrightnessMultiplierState;
finalBrightness = Math.Max( finalBrightness, 0f );
bool shouldBeEnabled = finalBrightness > 0.001f;
light.Enabled = shouldBeEnabled;
Color resolvedColor = ResolveLightColor( effectTime, absoluteTime );
light.LightColor = resolvedColor * finalBrightness;
light.Shadows = CastShadows && ( !isPlaying || distSq < ShadowMaxDistance * ShadowMaxDistance );
light.FogStrength = VolumetricBoost;
UpdateOutputState( shouldBeEnabled, true );
ManageAudio( shouldBeEnabled, finalBrightness / Math.Max( Brightness, 0.01f ) );
}
public void TurnOn()
{
SetEnabledState( true );
}
[Button]
public void ApplySelectedPreset()
{
ApplyPreset( SelectedPreset );
}
public void TurnOff()
{
SetEnabledState( false );
}
public void Toggle()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
IsEnabled = !IsEnabled;
}
public void SetPowered( bool powered )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
PoweredState = powered;
}
public void SetExternalBrightness( float multiplier )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ExternalBrightnessMultiplierState = Math.Max( multiplier, 0f );
}
public void ResetExternalBrightness()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ExternalBrightnessMultiplierState = 1.0f;
}
public void SetColorOverride( Color color )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
HasExternalColorOverrideState = true;
ExternalColorOverrideState = color;
}
public void ClearColorOverride()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
HasExternalColorOverrideState = false;
ExternalColorOverrideState = Color.White;
}
public void TriggerFlash( float duration = 0.15f, float brightnessMultiplier = 2f )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
FlashEndTimeState = RealTime.Now + Math.Max( duration, 0.01f );
FlashBrightnessMultiplierState = Math.Max( brightnessMultiplier, 1.0f );
}
public void TriggerAlarm( float duration = -1f )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
if ( duration <= 0f )
{
duration = DefaultAlarmDuration;
}
AlarmEndTimeState = Math.Max( AlarmEndTimeState, RealTime.Now + duration );
}
public void ClearAlarm()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
AlarmEndTimeState = 0f;
}
[Button]
public void PreviewFlash()
{
TriggerFlash();
}
[Button]
public void PreviewAlarm()
{
TriggerAlarm();
}
[Button]
public void ResetRuntimeOverrides()
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ClearAlarm();
ClearColorOverride();
ResetExternalBrightness();
SetPowered( true );
}
public void ApplyPreset( LightPreset preset )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
ApplyPresetInternal( preset );
}
public void SetEnabledState( bool isEnabled )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
IsEnabled = isEnabled;
}
public void SetBrightnessLevel( float brightness )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
Brightness = Math.Max( brightness, 0f );
}
public void SetLightColorValue( Color color )
{
if ( ShouldIgnoreNetworkMutation() )
{
return;
}
LightColor = color;
}
private void ApplyPresetInternal( LightPreset preset )
{
SelectedPreset = preset;
ResetPresetControlledFeatures();
switch ( preset )
{
case LightPreset.Candle:
Brightness = 0.75f;
LightColor = new Color( 1.0f, 0.76f, 0.5f );
SecondaryColor = new Color( 1.0f, 0.66f, 0.35f );
EnableKelvin = true;
KelvinTemperature = 1800;
EnableFire = true;
FireSpeed = 10.0f;
FireIntensity = 0.18f;
FireChaos = 0.6f;
VolumetricBoost = 0.6f;
break;
case LightPreset.Torch:
Brightness = 1.35f;
LightColor = new Color( 1.0f, 0.72f, 0.38f );
SecondaryColor = new Color( 1.0f, 0.45f, 0.2f );
EnableKelvin = true;
KelvinTemperature = 2200;
EnableFire = true;
FireSpeed = 13.0f;
FireIntensity = 0.28f;
FireChaos = 1.0f;
VolumetricBoost = 1.4f;
break;
case LightPreset.Neon:
Brightness = 1.15f;
LightColor = new Color( 0.2f, 0.95f, 1.0f );
SecondaryColor = new Color( 1.0f, 0.2f, 0.85f );
EnableColorTransition = true;
ColorTransitionSpeed = 0.65f;
EnablePulse = true;
PulseSpeed = 1.2f;
PulseMin = 0.75f;
CastShadows = false;
VolumetricBoost = 0.25f;
break;
case LightPreset.Alarm:
Brightness = 2.0f;
LightColor = new Color( 1.0f, 0.18f, 0.12f );
AlarmColor = LightColor;
EnableStrobe = true;
StrobeSpeed = 7.0f;
StrobeDutyCycle = 0.45f;
CastShadows = false;
VolumetricBoost = 1.1f;
break;
case LightPreset.BrokenLamp:
Brightness = 1.0f;
LightColor = new Color( 1.0f, 0.93f, 0.82f );
EnableHorror = true;
MinFlickerDelay = 0.04f;
MaxFlickerDelay = 0.25f;
DamageSeverity = 0.85f;
EnableKelvin = true;
KelvinTemperature = 3400;
break;
case LightPreset.SciFi:
Brightness = 1.6f;
LightColor = new Color( 0.35f, 0.78f, 1.0f );
SecondaryColor = new Color( 0.1f, 1.0f, 0.8f );
EnableColorTransition = true;
ColorTransitionSpeed = 1.15f;
EnablePulse = true;
PulseSpeed = 0.85f;
PulseMin = 0.55f;
VolumetricBoost = 2.0f;
break;
case LightPreset.StreetLight:
Brightness = 1.4f;
LightColor = new Color( 1.0f, 0.84f, 0.68f );
EnableKelvin = true;
KelvinTemperature = 3500;
MaxDistance = 4500.0f;
ShadowMaxDistance = 1200.0f;
VolumetricBoost = 0.55f;
break;
case LightPreset.Custom:
default:
break;
}
_lastAppliedPreset = preset;
_hasAppliedPreset = true;
}
private void ResetPresetControlledFeatures()
{
EnableFire = false;
EnableHorror = false;
EnableDisco = false;
EnableColorTransition = false;
EnablePulse = false;
EnableStrobe = false;
EnableKelvin = false;
}
private void SyncPresetSelectionIfNeeded()
{
bool autoApplyChanged = AutoApplyPreset != _lastObservedAutoApplyPreset;
bool presetChanged = SelectedPreset != _lastObservedPreset;
_lastObservedAutoApplyPreset = AutoApplyPreset;
_lastObservedPreset = SelectedPreset;
if ( !AutoApplyPreset )
{
return;
}
if ( autoApplyChanged || presetChanged || !_hasAppliedPreset || SelectedPreset != _lastAppliedPreset )
{
ApplyPresetInternal( SelectedPreset );
}
}
private void SyncComponentsIfNeeded( bool force = false )
{
bool missingActiveLight = TargetLightType == LightTypeEnum.Point ? _pointLight == null : _spotLight == null;
if ( !force && !missingActiveLight && TargetLightType == _lastSyncedLightType )
{
return;
}
Component createdComponent = null;
if ( TargetLightType == LightTypeEnum.Point )
{
if ( _pointLight == null )
{
_pointLight = Components.GetOrCreate<PointLight>();
createdComponent = _pointLight;
}
if ( _spotLight != null && _spotLight.Enabled )
{
_spotLight.Enabled = false;
}
}
else
{
if ( _spotLight == null )
{
_spotLight = Components.GetOrCreate<SpotLight>();
createdComponent = _spotLight;
}
if ( _pointLight != null && _pointLight.Enabled )
{
_pointLight.Enabled = false;
}
}
_lastSyncedLightType = TargetLightType;
if ( createdComponent != null && Game.IsPlaying && GameObject.NetworkMode == NetworkMode.Object )
{
GameObject.Network.Refresh( createdComponent );
}
}
private CameraComponent GetViewerCamera( float absoluteTime )
{
var sceneCamera = Scene.Camera;
if ( sceneCamera != null )
{
_cachedViewerCamera = sceneCamera;
_nextViewerCameraRefreshTime = absoluteTime + 0.25f;
return sceneCamera;
}
if ( _cachedViewerCamera != null && absoluteTime < _nextViewerCameraRefreshTime )
{
return _cachedViewerCamera;
}
_nextViewerCameraRefreshTime = absoluteTime + 0.25f;
_cachedViewerCamera = Scene.GetAllComponents<CameraComponent>().FirstOrDefault( camera => camera != null && camera.Enabled );
return _cachedViewerCamera;
}
private void EnsureNetworkMode()
{
if ( !ForceNetworkObjectMode || !Game.IsPlaying || GameObject.NetworkMode == NetworkMode.Object )
{
return;
}
GameObject.NetworkMode = NetworkMode.Object;
}
private bool ShouldIgnoreNetworkMutation()
{
return Game.IsPlaying && IsProxy;
}
private float GetFrameDelta( float absoluteTime )
{
float delta = Math.Clamp( absoluteTime - _lastUpdateTimestamp, 0.0001f, 0.25f );
_lastUpdateTimestamp = absoluteTime;
return delta;
}
private bool ShouldSkipAdaptiveUpdate( bool isPlaying, CameraComponent viewerCam, float distSq, float absoluteTime )
{
if ( !EnableAdaptiveUpdates || !isPlaying || viewerCam == null )
{
return false;
}
if ( absoluteTime < _nextAdaptiveUpdateTime )
{
return true;
}
float maxDistanceSq = Math.Max( MaxDistance * MaxDistance, 1f );
float distRatio = Math.Clamp( distSq / maxDistanceSq, 0f, 1f );
float nearInterval = 1f / Math.Max( NearUpdateRate, 1f );
float farInterval = 1f / Math.Max( FarUpdateRate, 1f );
_nextAdaptiveUpdateTime = absoluteTime + MathX.Lerp( nearInterval, farInterval, distRatio );
return false;
}
private void UpdateSensorWeight( CameraComponent viewerCam, float distSq, float deltaTime )
{
if ( EnableSensor && viewerCam != null && SensorRange > 0.01f )
{
float distRatio = Math.Clamp( 1.0f - (MathF.Sqrt( distSq ) / SensorRange), 0f, 1f );
float rawWeight = InvertSensor ? 1.0f - distRatio : distRatio;
_sensorWeightTarget = MathX.Lerp( SensorMinBrightness, SensorMaxBrightness, rawWeight );
}
else
{
_sensorWeightTarget = 1.0f;
}
_sensorWeightCurrent = MathX.Lerp( _sensorWeightCurrent, _sensorWeightTarget, Math.Clamp( deltaTime * SensorSmoothness, 0f, 1f ) );
}
private void UpdateEnabledBlend( float deltaTime )
{
float target = IsEnabled && PoweredState ? 1.0f : 0.0f;
if ( !EnableFade )
{
_enabledBlend = target;
return;
}
float duration = target > _enabledBlend ? Math.Max( FadeInDuration, 0.0001f ) : Math.Max( FadeOutDuration, 0.0001f );
float lerp = Math.Clamp( deltaTime / duration, 0f, 1f );
_enabledBlend = MathX.Lerp( _enabledBlend, target, lerp );
if ( Math.Abs( _enabledBlend - target ) < 0.001f )
{
_enabledBlend = target;
}
}
private void UpdateSway( float effectTime )
{
if ( EnableSway )
{
float pitch = MathF.Sin( effectTime * SwaySpeedPitch ) * SwayAmountPitch;
float roll = MathF.Cos( effectTime * SwaySpeedRoll ) * SwayAmountRoll;
LocalRotation = _baseRotation * Rotation.From( pitch, 0f, roll );
return;
}
LocalRotation = _baseRotation;
}
private float EvaluatePulseAndStrobe( float effectTime )
{
if ( EnableStrobe )
{
float cycle = (effectTime * StrobeSpeed) % 1.0f;
return cycle < StrobeDutyCycle ? 1.0f : 0.0f;
}
if ( EnablePulse )
{
float sine = (MathF.Sin( effectTime * PulseSpeed * 2.0f ) + 1.0f) * 0.5f;
return MathX.Lerp( PulseMin, 1.0f, sine );
}
return 1.0f;
}
private float EvaluatePattern( float effectTime )
{
if ( !EnablePattern || string.IsNullOrWhiteSpace( Pattern ) )
{
return 1.0f;
}
int index = (int)(effectTime * PatternSpeed) % Pattern.Length;
float value = Math.Max( 0, (char.ToLower( Pattern[index] ) - 'a') / 12.0f );
return value;
}
private float EvaluateFire( float effectTime )
{
if ( !EnableFire )
{
return 1.0f;
}
float noise = MathF.Sin( effectTime * FireSpeed ) + MathF.Sin( effectTime * FireSpeed * 0.5f );
if ( FireChaos > 0f )
{
noise += MathF.Sin( effectTime * FireSpeed * 1.5f ) * FireChaos;
}
return 1.0f - (noise * 0.15f * FireIntensity);
}
private float EvaluateHorror( float effectTime, bool isPlaying )
{
if ( !EnableHorror )
{
return 1.0f;
}
if ( effectTime > _nextFlicker )
{
bool isDamaged = Game.Random.Float( 0f, 1f ) < DamageSeverity;
_brokenMultiplier = isDamaged ? Game.Random.Float( 0.0f, 0.4f ) : 1.0f;
_nextFlicker = effectTime + Game.Random.Float( MinFlickerDelay, MaxFlickerDelay );
if ( isPlaying && isDamaged && SparkSound != null && _brokenMultiplier < 0.2f )
{
Sound.Play( SparkSound, WorldPosition );
}
}
return _brokenMultiplier;
}
private float EvaluatePowerSurge( float absoluteTime )
{
if ( !EnablePowerSurge )
{
return 1.0f;
}
if ( _nextSurgeTime <= 0f )
{
ScheduleNextSurge( absoluteTime );
}
if ( absoluteTime >= _nextSurgeTime )
{
_surgeEndTime = absoluteTime + Math.Max( SurgeDuration, 0.01f );
ScheduleNextSurge( _surgeEndTime );
}
return absoluteTime < _surgeEndTime ? Math.Max( SurgeBrightnessMultiplier, 1.0f ) : 1.0f;
}
private float EvaluateAlarm( float effectTime, float absoluteTime )
{
if ( absoluteTime >= AlarmEndTimeState )
{
return 1.0f;
}
float cycle = (effectTime * AlarmStrobeSpeed) % 1.0f;
float gate = cycle < 0.5f ? 1.0f : 0.15f;
return gate * Math.Max( AlarmBrightnessMultiplier, 0f );
}
private void ScheduleNextSurge( float absoluteTime )
{
float minInterval = Math.Min( SurgeMinInterval, SurgeMaxInterval );
float maxInterval = Math.Max( SurgeMinInterval, SurgeMaxInterval );
_nextSurgeTime = absoluteTime + Game.Random.Float( Math.Max( minInterval, 0.01f ), Math.Max( maxInterval, 0.01f ) );
}
private Color ResolveLightColor( float effectTime, float absoluteTime )
{
Color color = LightColor;
if ( EnableKelvin )
{
if ( KelvinTemperature != _lastKelvin )
{
_cachedKelvinColor = KelvinToColor( KelvinTemperature );
_lastKelvin = KelvinTemperature;
}
color = _cachedKelvinColor;
}
if ( EnableColorTransition )
{
float lerp = (MathF.Sin( effectTime * ColorTransitionSpeed ) + 1.0f) * 0.5f;
color = Color.Lerp( color, SecondaryColor, lerp, true );
}
if ( EnableDisco )
{
color = new ColorHsv( (effectTime * DiscoSpeed) % 360f, DiscoSaturation, DiscoValue ).ToColor();
}
if ( absoluteTime < AlarmEndTimeState )
{
color = Color.Lerp( color, AlarmColor, 0.85f, true );
}
if ( HasExternalColorOverrideState )
{
color = ExternalColorOverrideState;
}
return color;
}
private void UpdateOutputState( bool shouldBeEnabled, bool playOneShot )
{
if ( !_hasOutputState )
{
_hasOutputState = true;
_lastOutputEnabled = shouldBeEnabled;
return;
}
if ( _lastOutputEnabled == shouldBeEnabled )
{
return;
}
if ( playOneShot && Game.IsPlaying )
{
if ( shouldBeEnabled && ToggleOnSound != null )
{
Sound.Play( ToggleOnSound, WorldPosition );
}
else if ( !shouldBeEnabled && ToggleOffSound != null )
{
Sound.Play( ToggleOffSound, WorldPosition );
}
}
_lastOutputEnabled = shouldBeEnabled;
}
private void DisableOutput( Light light )
{
light.Enabled = false;
UpdateOutputState( false, false );
ManageAudio( false, 0f );
}
private void UpdateAmbientSoundPosition()
{
if ( _ambientSoundHandle != null && !_ambientSoundHandle.IsStopped )
{
_ambientSoundHandle.Position = WorldPosition;
}
}
private void ManageAudio( bool isLightEnabled, float intensityRatio )
{
if ( !Game.IsPlaying || AmbientSound == null )
{
return;
}
if ( isLightEnabled )
{
if ( _ambientSoundHandle == null || _ambientSoundHandle.IsStopped )
{
_ambientSoundHandle = Sound.Play( AmbientSound, WorldPosition );
}
if ( _ambientSoundHandle != null )
{
_ambientSoundHandle.Position = WorldPosition;
_ambientSoundHandle.Volume = BaseVolume * (ModulateVolumeWithLight ? intensityRatio : 1.0f);
_ambientSoundHandle.Pitch = ModulatePitchWithLight ? MathX.Lerp( MinPitch, MaxPitch, intensityRatio ) : 1.0f;
}
}
else if ( _ambientSoundHandle != null )
{
_ambientSoundHandle.Stop();
_ambientSoundHandle = null;
}
}
private Color KelvinToColor( int kelvin )
{
float temperature = kelvin / 100.0f;
float red;
float green;
float blue;
if ( temperature <= 66f )
{
red = 255f;
green = Math.Clamp( 99.47f * MathF.Log( temperature ) - 161.11f, 0f, 255f );
}
else
{
red = Math.Clamp( 329.698f * MathF.Pow( temperature - 60f, -0.133f ), 0f, 255f );
green = Math.Clamp( 288.12f * MathF.Pow( temperature - 60f, -0.075f ), 0f, 255f );
}
if ( temperature >= 66f )
{
blue = 255f;
}
else if ( temperature <= 19f )
{
blue = 0f;
}
else
{
blue = Math.Clamp( 138.51f * MathF.Log( temperature - 10f ) - 305.04f, 0f, 255f );
}
return new Color( red / 255f, green / 255f, blue / 255f );
}
protected override void DrawGizmos()
{
if ( !ShowDebugGizmos )
{
return;
}
Gizmo.Draw.Text( $"Group: {LightGroup}", new Transform( Vector3.Up * 20f ), size: 12 );
if ( !string.IsNullOrWhiteSpace( PowerGridTag ) )
{
Gizmo.Draw.Text( $"Grid: {PowerGridTag}", new Transform( Vector3.Up * 34f ), size: 12 );
}
if ( EnableSensor )
{
Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.2f );
Gizmo.Draw.SolidSphere( Vector3.Zero, SensorRange );
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.LineSphere( Vector3.Zero, SensorRange );
}
if ( EnableCulling )
{
Gizmo.Draw.Color = Color.Red.WithAlpha( 0.05f );
Gizmo.Draw.LineSphere( Vector3.Zero, MaxDistance );
Gizmo.Draw.Text( $"Cull: {MaxDistance}", new Transform( Vector3.Up * (MaxDistance * 0.9f) ), size: 14 );
}
}
protected override void OnDestroy()
{
if ( _ambientSoundHandle != null )
{
_ambientSoundHandle.Stop();
}
}
}
using PanelRenderTarget;
using Sandbox;
using Sandbox.UI;
using System;
using System.Linq;
public class TargetScreen : Component, Component.DontExecuteOnServer, ITargetScreen
{
[Property, Feature( "interaction" ), Description( "Enable verbose logs for screen mouse detection" )]
public bool DebugMouseTrace { get; set; } = false;
[Property] public string ScreenMaterialName { get; set; } = "screen-01";
[Property] public Material ScreenMaterial { get; set; } = Material.Load( "materials/screen.vmat" );
[Property] public Vector2Int ScreenTextureSize { get; set; } = new( 1280, 720 );
[Property] public float TraceDistance { get; set; } = 200f;
[Property] public bool ForceUpdate { get; set; } = false;
[Property] public PanelTypeReference PanelType { get; set; } = new();
[Property, Feature( "interaction" ), Description( "Small UV offset applied after triangle interpolation to correct slight drift" )]
public Vector2 ScreenUvOffset { get; set; } = Vector2.Zero;
[Property, Feature("interaction"), Description("interact with 2d mouse cursor")]
public bool ScreenCursorInteraction { get; set; } = false;
[Property,Feature("interaction"), Description("whether to show the virtual cursor") ]
public bool ShowVirtualCursor { get; set; } = true;
[Property, Feature( "Optimisation" ), Description("fps when the panel is focused") ]
public int UpdateRateFocus { get; set; } = 60;
[Property, Feature("Optimisation")]
public bool UpdateWhenNotFocused { get; set; } = false;
[Property, Feature( "Optimisation" ), Description( "fps update rate when the panel is visible in camera" ) ]
public int UpdateRateNotFocused { get; set; } = 30;
[Property, Feature( "Optimisation" ), Description("the distance of the update rate visible but not focus") ]
public int UpdateDistanceMax { get; set; } = 500;
private bool _firstUpdate = false;
private readonly TargetPanelInput _input = new();
public ModelRenderer Renderer { get; private set; }
private Material _screenMaterialCopy;
private Texture _screenTexture;
private TargetRootPanel _rootPanel;
private PanelSceneObject _panelObject;
private Vertex[] _cachedVertices;
private uint[] _cachedIndices;
private Model _cachedMeshModel;
private TriangleMaterialRange[] _cachedTriangleMaterialRanges = Array.Empty<TriangleMaterialRange>();
private double _tracePerfAccumMs;
private double _tracePerfMaxMs;
private double _tracePerfCacheAccumMs;
private double _tracePerfTrianglesAccumMs;
private int _tracePerfSamples;
private TimeSince _tracePerfLogTimer;
private readonly struct TriangleMaterialRange
{
public int StartTriangle { get; init; }
public int EndTriangleExclusive { get; init; }
public Material Material { get; init; }
}
protected Panel Panel { get; private set; }
public TargetRootPanel RootPanel => _rootPanel;
public PanelSceneObject PanelObject => _panelObject;
protected Texture ScreenTexture => _screenTexture;
protected override void OnPreRender()
{
//ensure sceneobject have transform and correct bound for engine culling
_panelObject.Transform = Renderer.Transform.World;
_panelObject.Bounds = Renderer.Bounds;
}
protected override void OnEnabled()
{
base.OnEnabled();
Renderer = Components.Get<ModelRenderer>();
if ( !Renderer.IsValid() )
{
Log.Warning( "No ModelRenderer found." );
Enabled = false;
return;
}
RefreshMeshCache();
CreateTexture();
CreatePanel();
SetupMaterial();
CreatePanelObject();
OnPanelCreated( Panel );
var panelComponent = Components.Get<PanelComponent>();
TargetPanelSystem.Current.RegisterScreen( this );
}
public Panel GetPanel()
{
return Panel;
}
protected virtual void OnPanelCreated( Panel panel )
{
}
private void CreateTexture()
{
_screenTexture = Texture.CreateRenderTarget()
.WithSize( ScreenTextureSize.x, ScreenTextureSize.y )
.WithInitialColor( Color.Black )
.WithMips()
.Create();
}
private void CreatePanel()
{
var bounds = new Rect( 0, 0, ScreenTextureSize.x, ScreenTextureSize.y );
_rootPanel = new TargetRootPanel
{
RenderedManually = true,
FixedBounds = bounds,
FixedScale = 1f,
PanelBounds = bounds,
MouseVisibility = ScreenCursorInteraction ? MouseVisibility.Visible : MouseVisibility.Hidden
};
_rootPanel.Style.Width = Length.Pixels( ScreenTextureSize.x );
_rootPanel.Style.Height = Length.Pixels( ScreenTextureSize.y );
var type = PanelType?.Resolve();
if ( type?.TargetType is null || !typeof( Panel ).IsAssignableFrom( type.TargetType ) )
{
Log.Warning( $"Invalid panel type: {PanelType?.TypeName}" );
return;
}
Panel = type.Create<Panel>();
_rootPanel.AddChild( Panel );
Panel.Style.Width = Length.Percent( 100 );
Panel.Style.Height = Length.Percent( 100 );
}
private void CreatePanelObject()
{
_panelObject = new PanelSceneObject(
GameObject.GetBounds(),
Scene.SceneWorld,
_rootPanel,
_screenTexture,
this
);
}
public void Tick()
{
if ( !Renderer.IsValid() || Renderer.Model is null || !Panel.IsValid() )
return;
var camera = Scene.Camera;
if ( camera is null )
return;
if ( !TryGetPanelPosition( camera, out var panelPos ) )
{
ClearInput();
return;
}
_panelObject.CursorPosition = panelPos;
_input.Tick(
_rootPanel,
panelPos,
Input.Down( "attack1" ),
Input.MouseWheel
);
}
private bool TryGetPanelPosition( CameraComponent camera, out Vector2 panelPos )
{
panelPos = default;
var screenCenter = new Vector2(
camera.ScreenRect.Size.x * 0.5f,
camera.ScreenRect.Size.y * 0.5f
);
var ray = camera.ScreenPixelToRay( screenCenter );
if( ScreenCursorInteraction )
{
ray = camera.ScreenPixelToRay( Mouse.Position );
}
return TryGetPanelPositionFromMeshRaycast( ray, out panelPos );
}
private bool TryGetPanelPositionFromMeshRaycast( Ray ray, out Vector2 panelPos )
{
using var scope = Sandbox.Diagnostics.Performance.Scope( "TargetScreen.MouseTrace" );
var totalTimer = Sandbox.Diagnostics.FastTimer.StartNew();
panelPos = default;
var cacheTimer = Sandbox.Diagnostics.FastTimer.StartNew();
RefreshMeshCache();
var cacheMs = cacheTimer.ElapsedMilliSeconds;
if ( _cachedVertices is null || _cachedIndices is null || _cachedIndices.Length < 3 )
{
if ( DebugMouseTrace )
Log.Info( $"[TargetScreen] Reject mesh cache: vertices={_cachedVertices?.Length ?? 0} indices={_cachedIndices?.Length ?? 0}" );
UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, 0f );
return false;
}
var localOrigin = Renderer.Transform.World.PointToLocal( ray.Position );
var localEnd = Renderer.Transform.World.PointToLocal( ray.Position + ray.Forward * TraceDistance );
var localDirection = (localEnd - localOrigin).Normal;
var bestDistance = float.MaxValue;
var bestUv = Vector2.Zero;
var bestTriangle = -1;
var triangleTimer = Sandbox.Diagnostics.FastTimer.StartNew();
for ( int triStart = 0; triStart + 2 < _cachedIndices.Length; triStart += 3 )
{
var triangleIndex = triStart / 3;
if ( !IsTriangleMaterialMatch( triangleIndex ) )
continue;
var i0 = _cachedIndices[triStart + 0];
var i1 = _cachedIndices[triStart + 1];
var i2 = _cachedIndices[triStart + 2];
if ( i0 >= _cachedVertices.Length || i1 >= _cachedVertices.Length || i2 >= _cachedVertices.Length )
continue;
var v0 = _cachedVertices[i0];
var v1 = _cachedVertices[i1];
var v2 = _cachedVertices[i2];
if ( !TryRayTriangleIntersection( localOrigin, localDirection, v0.Position, v1.Position, v2.Position, out var distance, out var barycentric ) )
continue;
if ( distance > TraceDistance || distance >= bestDistance )
continue;
var triangleUv =
v0.TexCoord0 * barycentric.x +
v1.TexCoord0 * barycentric.y +
v2.TexCoord0 * barycentric.z;
bestDistance = distance;
bestUv = triangleUv;
bestTriangle = triangleIndex;
}
var triangleMs = triangleTimer.ElapsedMilliSeconds;
if ( bestTriangle < 0 )
{
if ( DebugMouseTrace )
Log.Info( $"[TargetScreen] No triangle hit. vertices={_cachedVertices.Length} indices={_cachedIndices.Length}" );
UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, triangleMs );
return false;
}
if ( DebugMouseTrace )
Log.Info( $"[TargetScreen] Mesh hit triangle={bestTriangle} distance={bestDistance} rawUv={bestUv}" );
var uv = new Vector2(
bestUv.x - MathF.Floor( bestUv.x ),
bestUv.y - MathF.Floor( bestUv.y )
);
uv += ScreenUvOffset;
panelPos = new Vector2(
Math.Clamp( uv.x, 0f, 1f ) * ScreenTextureSize.x,
Math.Clamp( uv.y, 0f, 1f ) * ScreenTextureSize.y
);
if ( DebugMouseTrace )
Log.Info( $"[TargetScreen] Final UV={uv} panelPos={panelPos}" );
UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, triangleMs );
return true;
}
private void UpdateDebugTracePerf( double totalMs, double cacheMs, double triangleMs )
{
_tracePerfAccumMs += totalMs;
_tracePerfCacheAccumMs += cacheMs;
_tracePerfTrianglesAccumMs += triangleMs;
_tracePerfMaxMs = Math.Max( _tracePerfMaxMs, totalMs );
_tracePerfSamples++;
if ( _tracePerfLogTimer < 5f )
return;
var avgTotalMs = _tracePerfSamples > 0 ? _tracePerfAccumMs / _tracePerfSamples : 0d;
var avgCacheMs = _tracePerfSamples > 0 ? _tracePerfCacheAccumMs / _tracePerfSamples : 0d;
var avgTrianglesMs = _tracePerfSamples > 0 ? _tracePerfTrianglesAccumMs / _tracePerfSamples : 0d;
var callsPerSecond = _tracePerfLogTimer > 0f ? _tracePerfSamples / _tracePerfLogTimer : 0f;
var triangleCount = _cachedIndices?.Length / 3 ?? 0;
Log.Info( $"[TargetScreen] Trace perf avg={avgTotalMs:F3}ms max={_tracePerfMaxMs:F3}ms cache={avgCacheMs:F3}ms triangles={avgTrianglesMs:F3}ms calls={callsPerSecond:F1}/s tris={triangleCount}" );
_tracePerfAccumMs = 0d;
_tracePerfCacheAccumMs = 0d;
_tracePerfTrianglesAccumMs = 0d;
_tracePerfMaxMs = 0d;
_tracePerfSamples = 0;
_tracePerfLogTimer = 0f;
}
private void RefreshMeshCache()
{
var model = Renderer?.Model;
if ( model is null || model == _cachedMeshModel )
return;
_cachedMeshModel = model;
_cachedVertices = model.GetVertices();
_cachedIndices = model.GetIndices();
_cachedTriangleMaterialRanges = BuildTriangleMaterialRanges( model );
if ( DebugMouseTrace )
Log.Info( $"[TargetScreen] Refreshed mesh cache for {model.Name}: vertices={_cachedVertices?.Length ?? 0} indices={_cachedIndices?.Length ?? 0} materialRanges={_cachedTriangleMaterialRanges.Length}" );
}
private TriangleMaterialRange[] BuildTriangleMaterialRanges( Model model )
{
var meshInfo = model.MeshInfo;
if ( meshInfo?.Meshes is null )
return Array.Empty<TriangleMaterialRange>();
var ranges = new System.Collections.Generic.List<TriangleMaterialRange>();
var triangleCursor = 0;
foreach ( var mesh in meshInfo.Meshes )
{
if ( mesh?.DrawCalls is null )
continue;
foreach ( var drawCall in mesh.DrawCalls )
{
var triangleCount = Math.Max( 0, drawCall.Indices / 3 );
if ( triangleCount == 0 )
continue;
ranges.Add( new TriangleMaterialRange
{
StartTriangle = triangleCursor,
EndTriangleExclusive = triangleCursor + triangleCount,
Material = drawCall.Material
} );
if ( DebugMouseTrace )
Log.Info( $"[TargetScreen] Material range {triangleCursor}->{triangleCursor + triangleCount} material={drawCall.Material?.Name}" );
triangleCursor += triangleCount;
}
}
return ranges.ToArray();
}
private bool IsTriangleMaterialMatch( int triangleIndex )
{
if ( string.IsNullOrWhiteSpace( ScreenMaterialName ) || _cachedTriangleMaterialRanges.Length == 0 )
return true;
foreach ( var range in _cachedTriangleMaterialRanges )
{
if ( triangleIndex < range.StartTriangle || triangleIndex >= range.EndTriangleExclusive )
continue;
var match = range.Material?.Name?.Contains( ScreenMaterialName, StringComparison.OrdinalIgnoreCase ) == true;
if ( DebugMouseTrace && triangleIndex == range.StartTriangle )
Log.Info( $"[TargetScreen] Triangle {triangleIndex} material={range.Material?.Name} match={match}" );
return match;
}
return true;
}
private static bool TryRayTriangleIntersection( Vector3 origin, Vector3 direction, Vector3 a, Vector3 b, Vector3 c, out float distance, out Vector3 barycentric )
{
distance = 0f;
barycentric = default;
const float epsilon = 0.0001f;
var edge1 = b - a;
var edge2 = c - a;
var pvec = Vector3.Cross( direction, edge2 );
var det = Vector3.Dot( edge1, pvec );
if ( MathF.Abs( det ) < epsilon )
return false;
var invDet = 1f / det;
var tvec = origin - a;
var v = Vector3.Dot( tvec, pvec ) * invDet;
if ( v < 0f || v > 1f )
return false;
var qvec = Vector3.Cross( tvec, edge1 );
var w = Vector3.Dot( direction, qvec ) * invDet;
if ( w < 0f || v + w > 1f )
return false;
distance = Vector3.Dot( edge2, qvec ) * invDet;
if ( distance < 0f )
return false;
barycentric = new Vector3( 1f - v - w, v, w );
return true;
}
private void SetupMaterial()
{
var oldMaterial = Renderer.Model.Materials
.FirstOrDefault( x => x.Name.Contains( ScreenMaterialName ) );
var index = Renderer.Model.Materials.IndexOf( oldMaterial );
if ( index < 0 )
{
Log.Warning( $"Screen material not found: {ScreenMaterialName}" );
return;
}
_screenMaterialCopy = ScreenMaterial.CreateCopy();
_screenMaterialCopy.Set( "g_tColor", _screenTexture );
Renderer.Materials.SetOverride( index, _screenMaterialCopy );
}
private void ClearInput()
{
_input.Clear();
}
protected override void OnDisabled()
{
base.OnDisabled();
ClearInput();
_panelObject?.Delete();
_panelObject = null;
_rootPanel?.Delete( true );
_rootPanel = null;
_screenTexture?.Dispose();
_screenTexture = null;
_screenMaterialCopy = null;
Panel = null;
Renderer = null;
TargetPanelSystem.Current.UnregisterScreen( this );
}
}
using System;
using Sandbox;
namespace Goo.Animation;
public record struct SmoothVector2
{
public Vector2 Current;
public Vector2 Target;
public Vector2 Velocity;
public float SmoothTime;
public SmoothVector2(Vector2 initial, float smoothTime)
{
Current = initial;
Target = initial;
Velocity = default;
SmoothTime = smoothTime;
}
public void Update(float dt)
{
float vx = Velocity.x, vy = Velocity.y;
Current = new Vector2(
MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
Velocity = new Vector2(vx, vy);
}
public bool IsSettled =>
MathF.Abs(Target.x - Current.x) < 0.0001f &&
MathF.Abs(Target.y - Current.y) < 0.0001f &&
MathF.Abs(Velocity.x) < 0.0001f &&
MathF.Abs(Velocity.y) < 0.0001f;
/// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;
namespace Goo.Animation;
public readonly record struct Tween
{
public Sandbox.Utility.Easing.Function Easing { get; init; }
public float Duration { get; init; }
public float Delay { get; init; }
public float SpeedScale { get; init; }
public int Iterations { get; init; }
public bool PingPong { get; init; }
public bool Reversed { get; init; }
public Tween(Sandbox.Utility.Easing.Function easing, float duration, float delay = 0f)
{
Easing = easing;
Duration = duration;
Delay = delay;
SpeedScale = 1f;
Iterations = 1;
PingPong = false;
Reversed = false;
}
/// <summary>
/// Bridge a designer-authored Sandbox.Curve (authored over [0, 1]) into a Tween.
/// Allocates one delegate per call; cache the result in a static readonly field.
/// </summary>
public static Tween FromCurve(Sandbox.Curve curve, float duration, float delay = 0f)
=> new Tween(curve.Evaluate, duration, delay);
public float Eval(float elapsedSec)
{
if (Duration <= 0f) return Reversed ? 0f : 1f;
float t = (elapsedSec - Delay) * SpeedScale;
if (t <= 0f) return Reversed ? 1f : 0f;
float cycleDuration = PingPong ? 2f * Duration : Duration;
if (Iterations > 0 && t >= cycleDuration * Iterations)
{
float endLocal = PingPong ? 0f : 1f;
if (Reversed) endLocal = 1f - endLocal;
return Easing(endLocal);
}
float cycleT = t % cycleDuration;
float local = PingPong
? (cycleT < Duration ? cycleT / Duration : 1f - (cycleT - Duration) / Duration)
: cycleT / Duration;
if (Reversed) local = 1f - local;
return Easing(local);
}
}
public static class TweenExtensions
{
public static Tween Loop(this Tween t) => t with { Iterations = -1 };
public static Tween Times(this Tween t, int n) => t with { Iterations = n };
public static Tween PingPong(this Tween t) => t with { PingPong = true };
public static Tween Scale(this Tween t, float speed) => t with { SpeedScale = speed };
public static Tween WithDelay(this Tween t, float s) => t with { Delay = s };
public static Tween Reverse(this Tween t) => t with { Reversed = true };
}
using System;
using System.Collections;
namespace Goo;
public sealed class Children : IEnumerable
{
internal FrameList? _list;
internal int _buildId;
internal Children() { }
internal int Count { get { EnsureValid(); return _list!.Count; } }
internal ref Frame this[int i] { get { EnsureValid(); return ref _list![i]; } }
public void Add<T>(in T child) where T : struct, IBlob
{
EnsureValid();
ref Frame slot = ref _list!.Reserve();
child.WriteTo(ref slot);
}
void EnsureValid()
{
var ctx = BuildContext._current;
if (ctx == null || _list == null || _buildId != ctx._currentBuildId)
throw new InvalidOperationException(
"Container reused across rebuilds. The same Container instance cannot survive " +
"past the Build() it was created in. Build a new one inside Build(), or extract " +
"a helper function that returns a fresh Container each call.");
}
// IEnumerable is required by C# collection-initializer syntax; iteration is not a
// use case for Children, so this returns an empty enumerator.
IEnumerator IEnumerable.GetEnumerator() => Array.Empty<object>().GetEnumerator();
}
namespace Goo;
/// <summary>Compile-time constraint for blob struct types. Never use as storage, return, or parameter type (boxes the struct, destroys per-Rebuild allocation profile); only valid in where T : struct, IBlob.</summary>
public interface IBlob
{
static abstract BlobKind Kind { get; }
string? Key { get; }
internal void WriteTo(ref Frame frame);
}
/// <summary>Returns the single root Blob for a GooView build. A named delegate rather than
/// Func<IBlob> because IBlob has a static-abstract member (Kind) and C# bars such interfaces
/// as generic type arguments (CS8920). Consequence for Razor markup: a bare method group cannot
/// bind (its natural type is the illegal Func<IBlob>), so write
/// <c>Build=@(new BlobBuilder(MyBuild))</c>. See docs/site/docs/gotchas.md.</summary>
public delegate IBlob BlobBuilder();
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;
namespace Goo;
public readonly partial record struct Text
{
public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
public Color? FontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FontColor, value, StyleValue.FromColor); }
public string? FontFamily { init => _style = StyleAccumulator.Add(_style, StyleField.FontFamily, value); }
public Length? FontSize { init => _style = StyleAccumulator.Add(_style, StyleField.FontSize, value); }
public FontSmooth? FontSmooth { init => _style = StyleAccumulator.Add(_style, StyleField.FontSmooth, value, StyleValue.FromFontSmooth); }
public FontStyle? FontStyle { init => _style = StyleAccumulator.Add(_style, StyleField.FontStyle, value, StyleValue.FromFontStyle); }
public FontVariantNumeric? FontVariantNumeric { init => _style = StyleAccumulator.Add(_style, StyleField.FontVariantNumeric, value, StyleValue.FromFontVariantNumeric); }
public int? FontWeight { init => _style = StyleAccumulator.Add(_style, StyleField.FontWeight, value); }
public Length? LetterSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.LetterSpacing, value); }
public Length? LineHeight { init => _style = StyleAccumulator.Add(_style, StyleField.LineHeight, value); }
public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
public TextAlign? TextAlign { init => _style = StyleAccumulator.Add(_style, StyleField.TextAlign, value, StyleValue.FromTextAlign); }
public Length? TextBackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.TextBackgroundAngle, value); }
public Color? TextDecorationColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationColor, value, StyleValue.FromColor); }
public TextDecoration? TextDecorationLine { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationLine, value, StyleValue.FromTextDecoration); }
public TextSkipInk? TextDecorationSkipInk { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationSkipInk, value, StyleValue.FromTextSkipInk); }
public TextDecorationStyle? TextDecorationStyle { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationStyle, value, StyleValue.FromTextDecorationStyle); }
public Length? TextDecorationThickness { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationThickness, value); }
public FilterMode? TextFilter { init => _style = StyleAccumulator.Add(_style, StyleField.TextFilter, value, StyleValue.FromFilterMode); }
public Length? TextLineThroughOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextLineThroughOffset, value); }
public TextOverflow? TextOverflow { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverflow, value, StyleValue.FromTextOverflow); }
public Length? TextOverlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverlineOffset, value); }
public Color? TextStrokeColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeColor, value, StyleValue.FromColor); }
public Length? TextStrokeWidth { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeWidth, value); }
public TextTransform? TextTransform { init => _style = StyleAccumulator.Add(_style, StyleField.TextTransform, value, StyleValue.FromTextTransform); }
public Length? TextUnderlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextUnderlineOffset, value); }
public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
public WhiteSpace? WhiteSpace { init => _style = StyleAccumulator.Add(_style, StyleField.WhiteSpace, value, StyleValue.FromWhiteSpace); }
public WordBreak? WordBreak { init => _style = StyleAccumulator.Add(_style, StyleField.WordBreak, value, StyleValue.FromWordBreak); }
public Length? WordSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.WordSpacing, value); }
public Color? HoverBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverBackgroundColor, value, StyleValue.FromColor); }
public Color? ActiveBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveBackgroundColor, value, StyleValue.FromColor); }
public Color? FocusBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusBackgroundColor, value, StyleValue.FromColor); }
public Color? HoverFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverFontColor, value, StyleValue.FromColor); }
public Color? ActiveFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveFontColor, value, StyleValue.FromColor); }
public Color? FocusFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusFontColor, value, StyleValue.FromColor); }
public int? TransitionMs { init => _style = StyleAccumulator.Add(_style, StyleField.TransitionMs, value); }
}
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;
namespace Goo;
public readonly partial record struct TextEntry
{
public FlexDirection? FlexDirection { init => _style = StyleAccumulator.Add(_style, StyleField.FlexDirection, value, StyleValue.FromFlexDirection); }
public Justify? JustifyContent { init => _style = StyleAccumulator.Add(_style, StyleField.JustifyContent, value, StyleValue.FromJustify); }
public Align? AlignItems { init => _style = StyleAccumulator.Add(_style, StyleField.AlignItems, value, StyleValue.FromAlign); }
public DisplayMode? Display { init => _style = StyleAccumulator.Add(_style, StyleField.Display, value, StyleValue.FromDisplay); }
public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
public Length? Padding { init => _style = StyleAccumulator.Add(_style, StyleField.Padding, value); }
public Length? PaddingLeft { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingLeft, value); }
public Length? PaddingTop { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingTop, value); }
public Length? PaddingRight { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingRight, value); }
public Length? PaddingBottom { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingBottom, value); }
public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
public Length? Gap { init => _style = StyleAccumulator.Add(_style, StyleField.Gap, value); }
public Length? RowGap { init => _style = StyleAccumulator.Add(_style, StyleField.RowGap, value); }
public Length? ColumnGap { init => _style = StyleAccumulator.Add(_style, StyleField.ColumnGap, value); }
public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
public Length? BorderRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRadius, value); }
public Length? BorderTopLeftRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopLeftRadius, value); }
public Length? BorderTopRightRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopRightRadius, value); }
public Length? BorderBottomRightRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomRightRadius, value); }
public Length? BorderBottomLeftRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomLeftRadius, value); }
public Align? AlignContent { init => _style = StyleAccumulator.Add(_style, StyleField.AlignContent, value, StyleValue.FromAlign); }
public Align? AlignSelf { init => _style = StyleAccumulator.Add(_style, StyleField.AlignSelf, value, StyleValue.FromAlign); }
public float? AspectRatio { init => _style = StyleAccumulator.Add(_style, StyleField.AspectRatio, value); }
public Length? BackdropFilterBlur { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterBlur, value); }
public Length? BackdropFilterBrightness { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterBrightness, value); }
public Length? BackdropFilterContrast { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterContrast, value); }
public Length? BackdropFilterHueRotate { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterHueRotate, value); }
public Length? BackdropFilterInvert { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterInvert, value); }
public Length? BackdropFilterSaturate { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterSaturate, value); }
public Length? BackdropFilterSepia { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterSepia, value); }
public Length? BackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundAngle, value); }
public string? BackgroundBlendMode { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundBlendMode, value); }
public Texture? BackgroundImage { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundImage, value); }
public bool? BackgroundPlaybackPaused { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPlaybackPaused, value); }
public Length? BackgroundPositionX { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPositionX, value); }
public Length? BackgroundPositionY { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPositionY, value); }
public BackgroundRepeat? BackgroundRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundRepeat, value, StyleValue.FromBackgroundRepeat); }
public Length? BackgroundSizeX { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundSizeX, value); }
public Length? BackgroundSizeY { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundSizeY, value); }
public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
public Color? BorderBottomColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomColor, value, StyleValue.FromColor); }
public Color? BorderColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderColor, value, StyleValue.FromColor); }
public Color? BorderLeftColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderLeftColor, value, StyleValue.FromColor); }
public Color? BorderRightColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRightColor, value, StyleValue.FromColor); }
public Color? BorderTopColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopColor, value, StyleValue.FromColor); }
public BorderImageFill? BorderImageFill { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageFill, value, StyleValue.FromBorderImageFill); }
public BorderImageRepeat? BorderImageRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageRepeat, value, StyleValue.FromBorderImageRepeat); }
public Texture? BorderImageSource { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageSource, value); }
public Color? BorderImageTint { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageTint, value, StyleValue.FromColor); }
public Length? BorderImageWidthBottom { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthBottom, value); }
public Length? BorderImageWidthLeft { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthLeft, value); }
public Length? BorderImageWidthRight { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthRight, value); }
public Length? BorderImageWidthTop { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthTop, value); }
public Length? BorderBottomWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomWidth, value); }
public Length? BorderLeftWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderLeftWidth, value); }
public Length? BorderRightWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRightWidth, value); }
public Length? BorderTopWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopWidth, value); }
public Length? BorderWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderWidth, value); }
public Length? Bottom { init => _style = StyleAccumulator.Add(_style, StyleField.Bottom, value); }
public Length? Left { init => _style = StyleAccumulator.Add(_style, StyleField.Left, value); }
public Length? Right { init => _style = StyleAccumulator.Add(_style, StyleField.Right, value); }
public Length? Top { init => _style = StyleAccumulator.Add(_style, StyleField.Top, value); }
public Color? CaretColor { init => _style = StyleAccumulator.Add(_style, StyleField.CaretColor, value, StyleValue.FromColor); }
public string? Cursor { init => _style = StyleAccumulator.Add(_style, StyleField.Cursor, value); }
public Length? FilterBlur { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBlur, value); }
public Color? FilterBorderColor { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBorderColor, value, StyleValue.FromColor); }
public Length? FilterBorderWidth { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBorderWidth, value); }
public Length? FilterBrightness { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBrightness, value); }
public Length? FilterContrast { init => _style = StyleAccumulator.Add(_style, StyleField.FilterContrast, value); }
public Length? FilterHueRotate { init => _style = StyleAccumulator.Add(_style, StyleField.FilterHueRotate, value); }
public Length? FilterInvert { init => _style = StyleAccumulator.Add(_style, StyleField.FilterInvert, value); }
public Length? FilterSaturate { init => _style = StyleAccumulator.Add(_style, StyleField.FilterSaturate, value); }
public Length? FilterSepia { init => _style = StyleAccumulator.Add(_style, StyleField.FilterSepia, value); }
public Color? FilterTint { init => _style = StyleAccumulator.Add(_style, StyleField.FilterTint, value, StyleValue.FromColor); }
public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
public Wrap? FlexWrap { init => _style = StyleAccumulator.Add(_style, StyleField.FlexWrap, value, StyleValue.FromWrap); }
public Color? FontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FontColor, value, StyleValue.FromColor); }
public string? FontFamily { init => _style = StyleAccumulator.Add(_style, StyleField.FontFamily, value); }
public Length? FontSize { init => _style = StyleAccumulator.Add(_style, StyleField.FontSize, value); }
public FontSmooth? FontSmooth { init => _style = StyleAccumulator.Add(_style, StyleField.FontSmooth, value, StyleValue.FromFontSmooth); }
public FontStyle? FontStyle { init => _style = StyleAccumulator.Add(_style, StyleField.FontStyle, value, StyleValue.FromFontStyle); }
public FontVariantNumeric? FontVariantNumeric { init => _style = StyleAccumulator.Add(_style, StyleField.FontVariantNumeric, value, StyleValue.FromFontVariantNumeric); }
public int? FontWeight { init => _style = StyleAccumulator.Add(_style, StyleField.FontWeight, value); }
public ImageRendering? ImageRendering { init => _style = StyleAccumulator.Add(_style, StyleField.ImageRendering, value, StyleValue.FromImageRendering); }
public Length? LetterSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.LetterSpacing, value); }
public Length? LineHeight { init => _style = StyleAccumulator.Add(_style, StyleField.LineHeight, value); }
public Length? MaskAngle { init => _style = StyleAccumulator.Add(_style, StyleField.MaskAngle, value); }
public Texture? MaskImage { init => _style = StyleAccumulator.Add(_style, StyleField.MaskImage, value); }
public MaskMode? MaskMode { init => _style = StyleAccumulator.Add(_style, StyleField.MaskMode, value, StyleValue.FromMaskMode); }
public Length? MaskPositionX { init => _style = StyleAccumulator.Add(_style, StyleField.MaskPositionX, value); }
public Length? MaskPositionY { init => _style = StyleAccumulator.Add(_style, StyleField.MaskPositionY, value); }
public BackgroundRepeat? MaskRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.MaskRepeat, value, StyleValue.FromBackgroundRepeat); }
public MaskScope? MaskScope { init => _style = StyleAccumulator.Add(_style, StyleField.MaskScope, value, StyleValue.FromMaskScope); }
public Length? MaskSizeX { init => _style = StyleAccumulator.Add(_style, StyleField.MaskSizeX, value); }
public Length? MaskSizeY { init => _style = StyleAccumulator.Add(_style, StyleField.MaskSizeY, value); }
public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
public string? MixBlendMode { init => _style = StyleAccumulator.Add(_style, StyleField.MixBlendMode, value); }
public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
public int? Order { init => _style = StyleAccumulator.Add(_style, StyleField.Order, value); }
public Color? OutlineColor { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineColor, value, StyleValue.FromColor); }
public Length? OutlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineOffset, value); }
public Length? OutlineWidth { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineWidth, value); }
public OverflowMode? Overflow { init => _style = StyleAccumulator.Add(_style, StyleField.Overflow, value, StyleValue.FromOverflowMode); }
public OverflowMode? OverflowX { init => _style = StyleAccumulator.Add(_style, StyleField.OverflowX, value, StyleValue.FromOverflowMode); }
public OverflowMode? OverflowY { init => _style = StyleAccumulator.Add(_style, StyleField.OverflowY, value, StyleValue.FromOverflowMode); }
public Length? PerspectiveOriginX { init => _style = StyleAccumulator.Add(_style, StyleField.PerspectiveOriginX, value); }
public Length? PerspectiveOriginY { init => _style = StyleAccumulator.Add(_style, StyleField.PerspectiveOriginY, value); }
public PointerEvents? PointerEvents { init => _style = StyleAccumulator.Add(_style, StyleField.PointerEvents, value, StyleValue.FromPointerEvents); }
public PositionMode? Position { init => _style = StyleAccumulator.Add(_style, StyleField.Position, value, StyleValue.FromPositionMode); }
public string? SoundIn { init => _style = StyleAccumulator.Add(_style, StyleField.SoundIn, value); }
public string? SoundOut { init => _style = StyleAccumulator.Add(_style, StyleField.SoundOut, value); }
public TextAlign? TextAlign { init => _style = StyleAccumulator.Add(_style, StyleField.TextAlign, value, StyleValue.FromTextAlign); }
public Length? TextBackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.TextBackgroundAngle, value); }
public Color? TextDecorationColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationColor, value, StyleValue.FromColor); }
public TextDecoration? TextDecorationLine { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationLine, value, StyleValue.FromTextDecoration); }
public TextSkipInk? TextDecorationSkipInk { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationSkipInk, value, StyleValue.FromTextSkipInk); }
public TextDecorationStyle? TextDecorationStyle { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationStyle, value, StyleValue.FromTextDecorationStyle); }
public Length? TextDecorationThickness { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationThickness, value); }
public FilterMode? TextFilter { init => _style = StyleAccumulator.Add(_style, StyleField.TextFilter, value, StyleValue.FromFilterMode); }
public Length? TextLineThroughOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextLineThroughOffset, value); }
public TextOverflow? TextOverflow { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverflow, value, StyleValue.FromTextOverflow); }
public Length? TextOverlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverlineOffset, value); }
public Color? TextStrokeColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeColor, value, StyleValue.FromColor); }
public Length? TextStrokeWidth { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeWidth, value); }
public TextTransform? TextTransform { init => _style = StyleAccumulator.Add(_style, StyleField.TextTransform, value, StyleValue.FromTextTransform); }
public Length? TextUnderlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextUnderlineOffset, value); }
public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
public Length? TransformOriginX { init => _style = StyleAccumulator.Add(_style, StyleField.TransformOriginX, value); }
public Length? TransformOriginY { init => _style = StyleAccumulator.Add(_style, StyleField.TransformOriginY, value); }
public WhiteSpace? WhiteSpace { init => _style = StyleAccumulator.Add(_style, StyleField.WhiteSpace, value, StyleValue.FromWhiteSpace); }
public WordBreak? WordBreak { init => _style = StyleAccumulator.Add(_style, StyleField.WordBreak, value, StyleValue.FromWordBreak); }
public Length? WordSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.WordSpacing, value); }
public int? ZIndex { init => _style = StyleAccumulator.Add(_style, StyleField.ZIndex, value); }
public Color? HoverBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverBackgroundColor, value, StyleValue.FromColor); }
public Color? ActiveBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveBackgroundColor, value, StyleValue.FromColor); }
public Color? FocusBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusBackgroundColor, value, StyleValue.FromColor); }
public Color? HoverFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverFontColor, value, StyleValue.FromColor); }
public Color? ActiveFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveFontColor, value, StyleValue.FromColor); }
public Color? FocusFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusFontColor, value, StyleValue.FromColor); }
public int? TransitionMs { init => _style = StyleAccumulator.Add(_style, StyleField.TransitionMs, value); }
}
using Goo;
using Sandbox.UI;
namespace Sandbox;
public class CounterUI : GooPanel<Container>
{
private int _count;
protected override Container Build() => new Container
{
Padding = 16,
Width = 128,
Height = 128,
BackgroundColor = Color.White,
BorderRadius = 12,
FlexDirection = FlexDirection.Row,
Gap = 12,
AlignItems = Align.Center,
Children =
{
new Text(_count.ToString()),
new Container
{
Padding = 8,
BackgroundColor = Color.Orange,
HoverBackgroundColor = Color.Cyan,
BorderRadius = 6,
OnClick = e => { _count++; Rebuild(); },
Children = { new Text("+") },
},
},
};
}using System;
using System.Collections.Generic;
using Sandbox;
namespace Goo.Input;
// Polls a curated key catalog once per frame for rising-edge presses + modifier state. ReemitHeldOnModifierRise re-emits a held key when a modifier rises (so "hold W, press Ctrl" reads as a chord).
public sealed class KeyTracker
{
public bool ReemitHeldOnModifierRise { get; set; } = false;
public ModifierState Modifiers { get; private set; }
public IReadOnlyList<KeyDescriptor> JustPressed => _justPressed;
readonly IReadOnlyList<KeyDescriptor> _catalog;
readonly HashSet<string> _downLastFrame = new();
readonly List<KeyDescriptor> _justPressed = new();
public KeyTracker() : this( KnownKeys.All ) { }
public KeyTracker( IReadOnlyList<KeyDescriptor> catalog )
{
_catalog = catalog;
}
public void Reset()
{
_downLastFrame.Clear();
_justPressed.Clear();
Modifiers = default;
}
static readonly Func<string, bool> s_engineDown = Sandbox.Input.Keyboard.Down;
public void Poll() => Poll( s_engineDown );
// isDown seam exists because engine Input statics throw outside a running engine process.
public void Poll( Func<string, bool> isDown )
{
_justPressed.Clear();
var prev = Modifiers;
for ( int i = 0; i < _catalog.Count; i++ )
{
var d = _catalog[i];
bool down = isDown( d.EngineName );
if ( down && !_downLastFrame.Contains( d.EngineName ) )
_justPressed.Add( d );
if ( down ) _downLastFrame.Add( d.EngineName );
else _downLastFrame.Remove( d.EngineName );
}
Modifiers = new ModifierState(
_downLastFrame.Contains( "ctrl" ),
_downLastFrame.Contains( "shift" ),
_downLastFrame.Contains( "alt" ),
_downLastFrame.Contains( "win" ) );
if ( !ReemitHeldOnModifierRise ) return;
bool modRose = ( Modifiers.Ctrl && !prev.Ctrl ) || ( Modifiers.Shift && !prev.Shift )
|| ( Modifiers.Alt && !prev.Alt ) || ( Modifiers.Meta && !prev.Meta );
if ( !modRose ) return;
for ( int i = 0; i < _catalog.Count; i++ )
{
var d = _catalog[i];
if ( d.Class == KeyClass.Modifier ) continue;
if ( !_downLastFrame.Contains( d.EngineName ) ) continue;
if ( _justPressed.Contains( d ) ) continue;
_justPressed.Add( d );
}
}
/// <summary>True if the named key had a rising edge (just-pressed, not held) this frame; call Poll first.</summary>
public bool Pressed( string engineName )
{
for ( int i = 0; i < _justPressed.Count; i++ )
if ( _justPressed[i].EngineName == engineName ) return true;
return false;
}
}
using System;
using Sandbox;
using Sandbox.UI;
namespace Goo.Internal;
internal sealed class StatefulLabel : Label, IStatefulHost, IStatefulEventHost
{
StateController? _state;
internal Action<MousePanelEvent>? _onClick;
internal Action<MousePanelEvent>? _onRightClick;
internal Action<MousePanelEvent>? _onMiddleClick;
internal Action<MousePanelEvent>? _onMouseEnter;
internal Action<MousePanelEvent>? _onMouseLeave;
internal Action<MousePanelEvent>? _onMouseDown;
internal Action<MousePanelEvent>? _onMouseUp;
internal Action<MousePanelEvent>? _onMouseMove;
internal bool _userSetPointerEvents;
internal Action? _requestRebuild;
public Action? RequestRebuild { set => _requestRebuild = value; }
public void ApplyStateVariants(
Color? baseBg, Color? baseFg,
Color? hoverBg, Color? activeBg, Color? focusBg,
Color? hoverFg, Color? activeFg, Color? focusFg,
int? transitionMs)
{
_state ??= new StateController(this);
_state.ApplyVariants(
baseBg, baseFg,
hoverBg, activeBg, focusBg,
hoverFg, activeFg, focusFg,
transitionMs);
}
public void ClearStateVariants() => _state?.ClearVariants();
public bool HasActiveStateVariants => _state?.HasActiveVariants ?? false;
public void ApplyEvents(in BlobEvents events)
{
_onClick = events.OnClick;
_onRightClick = events.OnRightClick;
_onMiddleClick = events.OnMiddleClick;
_onMouseEnter = events.OnMouseEnter;
_onMouseLeave = events.OnMouseLeave;
_onMouseDown = events.OnMouseDown;
_onMouseUp = events.OnMouseUp;
_onMouseMove = events.OnMouseMove;
}
public bool HasEventHandlers =>
_onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
_onMouseDown != null || _onMouseUp != null || _onMouseMove != null;
public bool UserSetPointerEvents
{
get => _userSetPointerEvents;
set => _userSetPointerEvents = value;
}
protected override void OnClick(MousePanelEvent e)
{
base.OnClick(e);
EventDispatch.Fire(_onClick, e, _requestRebuild);
}
protected override void OnRightClick(MousePanelEvent e)
{
base.OnRightClick(e);
EventDispatch.Fire(_onRightClick, e, _requestRebuild);
}
protected override void OnMiddleClick(MousePanelEvent e)
{
base.OnMiddleClick(e);
EventDispatch.Fire(_onMiddleClick, e, _requestRebuild);
}
protected override void OnMouseOver(MousePanelEvent e)
{
base.OnMouseOver(e);
EventDispatch.Fire(_onMouseEnter, e, _requestRebuild);
}
protected override void OnMouseOut(MousePanelEvent e)
{
base.OnMouseOut(e);
EventDispatch.Fire(_onMouseLeave, e, _requestRebuild);
}
protected override void OnMouseDown(MousePanelEvent e)
{
base.OnMouseDown(e);
EventDispatch.Fire(_onMouseDown, e, _requestRebuild);
}
protected override void OnMouseUp(MousePanelEvent e)
{
base.OnMouseUp(e);
EventDispatch.Fire(_onMouseUp, e, _requestRebuild);
}
protected override void OnMouseMove(MousePanelEvent e)
{
base.OnMouseMove(e);
EventDispatch.Fire(_onMouseMove, e, _requestRebuild);
}
}
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;
namespace Goo;
/// <summary>
/// A custom shader applied to a Blob. Point it at a compiled .shader; it parses the shader
/// source's Attribute() declarations to validate uniform names and to reset uniforms other
/// panels have set (panels share one CommandList attribute namespace), and pushes the uniform
/// bag every frame. Set uniforms with the collection initializer (<c>["Name"] = value</c>);
/// a value may be a literal or a per-frame Func. Subclass and override <see cref="Apply"/>
/// only for bespoke per-frame CPU logic.
/// </summary>
public record ShaderEffect
{
static readonly Dictionary<object, Material> _materialCache = new();
static readonly Dictionary<object, ShaderSchemaInfo?> _schemaCache = new();
// Every uniform name any ShaderEffect has pushed this session, with the last value pushed
// (its runtime type drives the reset conversion). Panels share one CommandList attribute
// namespace, so a uniform set by one panel persists into every later panel's draw; an
// effect that does not set a seen uniform must reset it to its shader's declared default
// or it inherits the other panel's value (view-5u5m). Touched only from Apply (render thread).
static readonly Dictionary<string, object> _seenUniforms = new();
internal sealed class ShaderSchemaInfo
{
public required HashSet<string> Names; // declared attribute names
public required Dictionary<string, (Vector4 Floats, Vector4 Ints)> Defaults;
}
readonly string? _path;
readonly Shader? _shader;
readonly GrabMode _grab;
readonly Dictionary<string, UniformValue> _bag = new();
bool _validated;
/// <summary>For subclasses that supply their own <see cref="Material"/> and <see cref="Apply"/>.</summary>
protected ShaderEffect() { }
/// <summary>Apply the shader at <paramref name="shaderPath"/> (e.g. "shaders/ui_dither.shader").</summary>
public ShaderEffect( string shaderPath, GrabMode grab = GrabMode.None )
{
_path = shaderPath;
_grab = grab;
}
/// <summary>Apply a shader resource (drag-droppable in the inspector).</summary>
public ShaderEffect( Shader shader, GrabMode grab = GrabMode.None )
{
_shader = shader;
_grab = grab;
}
/// <summary>The shader asset path, or null when constructed from a <see cref="T:Sandbox.Shader"/> resource.</summary>
public string? ShaderPath => _path;
/// <summary>How this effect grabs the framebuffer behind its panel.</summary>
public GrabMode Grab => _grab;
/// <summary>Get or set a uniform by its shader attribute name.</summary>
public UniformValue this[string name]
{
get => _bag[name];
set => _bag[name] = value;
}
object CacheKey => (object?)_path ?? _shader!;
/// <summary>The material this effect draws with, cached so it is excluded from record equality.</summary>
public virtual Material Material
{
get
{
var key = CacheKey;
if ( !_materialCache.TryGetValue( key, out var mat ) )
{
mat = _path is not null ? Material.FromShader( _path ) : Material.FromShader( _shader! );
_materialCache[key] = mat;
}
return mat;
}
}
/// <summary>Create the Material now, on the calling thread. Call on the main thread; Draw runs on the render thread and must only read the cache.</summary>
public void Warm() => _ = Material;
/// <summary>Set this effect's shader attributes for the frame, then grab the framebuffer per <see cref="Grab"/>.</summary>
protected internal virtual void Apply( CommandList cl, Rect rect )
{
cl.Attributes.Set( "BoxSize", new Vector2( rect.Width, rect.Height ) );
// Subclass escape hatch: a derived effect calling base.Apply gets only BoxSize.
if ( _path is null && _shader is null ) return;
Validate();
// Reset seen-but-unset uniforms to this shader's declared defaults so another panel's
// attribute writes do not bleed into this draw (view-5u5m).
var schema = SchemaFor( CacheKey );
if ( schema is not null )
{
foreach ( var (name, sample) in _seenUniforms )
{
if ( _bag.ContainsKey( name ) ) continue;
if ( !schema.Defaults.TryGetValue( name, out var def ) ) continue;
if ( ResetValue( sample, def.Floats, def.Ints ) is { } reset )
SetAttribute( cl, name, reset );
}
}
foreach ( var (name, value) in _bag )
{
var resolved = value.Resolve();
SetAttribute( cl, name, resolved );
if ( resolved is not Texture )
_seenUniforms[name] = resolved;
}
switch ( _grab )
{
case GrabMode.Sharp:
cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture" );
break;
case GrabMode.Blurred:
cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture", Graphics.DownsampleMethod.GaussianBlur );
break;
}
}
static void SetAttribute( CommandList cl, string name, object value )
{
switch ( value )
{
case float f: cl.Attributes.Set( name, f ); break;
case bool b: cl.Attributes.Set( name, b ); break;
case Vector2 v: cl.Attributes.Set( name, v ); break;
case Vector3 v: cl.Attributes.Set( name, v ); break;
case Vector4 v: cl.Attributes.Set( name, v ); break;
case Color c: cl.Attributes.Set( name, c ); break;
case Texture t: cl.Attributes.Set( name, t ); break;
}
}
// Reads the shader's declared attribute names once per instance and warns on uniform names
// the shader does not declare. Skipped silently when the source is unavailable.
void Validate()
{
if ( _validated ) return;
_validated = true;
var valid = SchemaFor( CacheKey )?.Names;
if ( valid is null || valid.Count == 0 ) return;
foreach ( var name in _bag.Keys )
if ( !valid.Contains( name ) )
Sandbox.Internal.GlobalSystemNamespace.Log.Warning(
$"ShaderEffect for \"{_path ?? _shader?.ResourcePath}\": uniform \"{name}\" is not declared by the shader " +
$"(valid: {string.Join( ", ", valid )}). Ignored." );
}
// Names + defaults come from parsing the .shader SOURCE, which ships with the project and
// declares every Attribute() with its Default(). The engine's Shader.Schema is editor-only
// plumbing: at game runtime it throws ("Load must be called on the main thread!" — Apply
// runs on the render thread) so it is not consulted at all (view-5u5m probe, 2026-06-11).
// Null when the source is unreadable (e.g. unit tests, published build without raw
// .shader files); reset and validation both skip then.
static ShaderSchemaInfo? SchemaFor( object key )
{
if ( _schemaCache.TryGetValue( key, out var info ) ) return info;
info = null;
if ( (key as string ?? (key as Shader)?.ResourcePath) is { } srcPath )
{
try
{
info = ParseShaderSource( FileSystem.Mounted.ReadAllText( srcPath ) );
}
catch
{
info = null;
}
}
_schemaCache[key] = info;
return info;
}
static readonly System.Text.RegularExpressions.Regex _declRegex = new(
@"\b(?<type>float[234]?|bool|int|Texture2D)\s+\w+\s*<(?<block>[^>]*)>",
System.Text.RegularExpressions.RegexOptions.Compiled );
static readonly System.Text.RegularExpressions.Regex _attrRegex = new(
@"Attribute\s*\(\s*""(?<name>[^""]+)""\s*\)",
System.Text.RegularExpressions.RegexOptions.Compiled );
static readonly System.Text.RegularExpressions.Regex _defaultRegex = new(
@"Default[234]?\s*\(\s*(?<args>[^)]*)\)",
System.Text.RegularExpressions.RegexOptions.Compiled );
// Pure: extracts Attribute()-bound uniform declarations and their Default() values from
// .shader source. Defaults are stored in both vector slots so ResetValue can convert by the
// bled value's runtime type. Texture declarations contribute a name (for validation) but no
// default. Only the main file is scanned; #include'd uniforms (BoxSize, DpiScale) are
// framework-managed and excluded anyway. Missing Default() = zeros, matching the engine's
// unset-attribute behavior.
internal static ShaderSchemaInfo? ParseShaderSource( string source )
{
var names = new HashSet<string>();
var defaults = new Dictionary<string, (Vector4 Floats, Vector4 Ints)>();
foreach ( System.Text.RegularExpressions.Match m in _declRegex.Matches( source ) )
{
var block = m.Groups["block"].Value;
var attr = _attrRegex.Match( block );
if ( !attr.Success ) continue;
var name = attr.Groups["name"].Value;
names.Add( name );
if ( name is "BoxSize" or "DpiScale" ) continue; // framework-managed per draw
if ( m.Groups["type"].Value == "Texture2D" ) continue; // no resettable default
Vector4 v = default;
var def = _defaultRegex.Match( block );
if ( def.Success )
{
var parts = def.Groups["args"].Value.Split( ',' );
for ( int i = 0; i < parts.Length && i < 4; i++ )
{
if ( !float.TryParse( parts[i].Trim(), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var f ) ) continue;
switch ( i )
{
case 0: v.x = f; break;
case 1: v.y = f; break;
case 2: v.z = f; break;
case 3: v.w = f; break;
}
}
}
defaults[name] = (v, v);
}
return names.Count > 0 ? new ShaderSchemaInfo { Names = names, Defaults = defaults } : null;
}
// Pure: converts a shader's declared default (schema FloatDefault/IntDefault vectors) to the
// runtime type of the value that bled in, so the reset lands in the same attribute slot type.
// Null = no safe reset (unsupported type; textures are never recorded so never reach this).
internal static object? ResetValue( object sample, Vector4 floats, Vector4 ints ) => sample switch
{
float => floats.x,
bool => ints.x != 0f,
Vector2 => new Vector2( floats.x, floats.y ),
Vector3 => new Vector3( floats.x, floats.y, floats.z ),
Color => new Color( floats.x, floats.y, floats.z, floats.w ),
Vector4 => floats,
_ => null,
};
public virtual bool Equals( ShaderEffect? other )
{
if ( other is null ) return false;
if ( ReferenceEquals( this, other ) ) return true;
if ( EqualityContract != other.EqualityContract ) return false;
return _path == other._path
&& ReferenceEquals( _shader, other._shader )
&& _grab == other._grab
&& BagEquals( _bag, other._bag );
}
public override int GetHashCode()
{
var hc = new HashCode();
hc.Add( EqualityContract );
hc.Add( _path );
hc.Add( _grab );
hc.Add( _bag.Count );
return hc.ToHashCode();
}
static bool BagEquals( Dictionary<string, UniformValue> a, Dictionary<string, UniformValue> b )
{
if ( a.Count != b.Count ) return false;
foreach ( var (k, v) in a )
if ( !b.TryGetValue( k, out var bv ) || !v.Equals( bv ) ) return false;
return true;
}
}
using Sandbox.Citizen;
namespace ShrimpleCharacterController;
[Hide]
public sealed class ShrimpleFlyer : Component
{
[RequireComponent]
public ShrimpleCharacterController Controller { get; set; }
public GameObject Camera { get; set; }
[Property]
[Range(400f, 1600f)]
public float WalkSpeed { get; set; } = 800f;
[Property]
[Range(800f, 4000f)]
public float RunSpeed { get; set; } = 2400f;
public Angles EyeAngles { get; set; }
protected override void OnStart()
{
base.OnStart();
Camera = new GameObject(true, "Camera");
Camera.SetParent(GameObject);
var cameraComponent = Camera.Components.Create<CameraComponent>();
cameraComponent.ZFar = 32768f;
}
protected override void OnFixedUpdate()
{
base.OnFixedUpdate();
var isDucking = Input.Down("Duck");
var isRunning = Input.Down("Run");
var ascending = Input.Down("Jump") ? 1f : 0f;
var descending = Input.Down("Duck") ? -1f : 0f;
var wishSpeed = isRunning ? RunSpeed : WalkSpeed;
var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();
Controller.WishVelocity = wishDirection * wishSpeed;
Controller.Move();
}
protected override void OnUpdate()
{
base.OnUpdate();
EyeAngles += Input.AnalogLook;
EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -40f, 40f));
var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 760f;
Camera.WorldRotation = EyeAngles.ToRotation();
Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "igor" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "igor.reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-27T21:09:21.7423840Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.111.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.111.0")]using System.Runtime.CompilerServices;
namespace Sandbox.Reactivity.Internals.Runtimes;
internal sealed class Runtime : IDisposable
{
/// <summary>
/// Effects that are waiting to run due to reactivity changes.
/// </summary>
private readonly Queue<Effect> _pendingEffects = new(16);
/// <summary>
/// How many effects have run during a flush operation.
/// </summary>
private uint _flushDepth;
/// <summary>
/// Whether pending effects are currently being run.
/// </summary>
private bool _isFlushing;
/// <summary>
/// The currently executing effect.
/// </summary>
public Effect? CurrentEffect { get; set; }
/// <summary>
/// The currently executing reaction.
/// </summary>
public IReaction? CurrentReaction { get; set; }
/// <summary>
/// A monotonically increasing counter that's incremented when an <see cref="IProducer" /> updates its current
/// value.
/// </summary>
public uint Version { get; set; } = 1;
/// <summary>
/// Whether dependency tracking is currently disabled.
/// </summary>
public bool IsUntracking { get; set; }
/// <summary>
/// Whether a flush was scheduled to run at the end of the frame.
/// </summary>
public bool IsFlushScheduled { get; private set; }
/// <summary>
/// Whether an effect is currently executing its teardown function. This is used by producers to skip any
/// recomputation when accessed to ensure the previous value is returned.
/// </summary>
public bool IsRunningTeardown { get; set; }
public void Dispose()
{
_pendingEffects.Clear();
CurrentEffect = null;
CurrentReaction = null;
Version = uint.MaxValue;
IsUntracking = true;
IsFlushScheduled = false;
_isFlushing = false;
}
/// <summary>
/// Returns the currently executing effect.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if there is no effect that is currently executing.
/// </exception>
public Effect EnsureCurrentEffect([CallerMemberName] string name = "Effect")
{
return CurrentEffect ?? throw new InvalidOperationException(name + " must be created in an effect root");
}
public void ScheduleEffect(Effect effect)
{
_pendingEffects.Enqueue(effect);
if (!IsFlushScheduled && !_isFlushing)
{
IsFlushScheduled = true;
}
}
/// <summary>
/// Empties the queue of effects that are scheduled to run due to one of their dependencies changing. This should
/// only be run when you want an effect to re-run immediately after changing a reactive value.
/// </summary>
public void Flush()
{
if (_isFlushing)
{
return;
}
_isFlushing = true;
IsFlushScheduled = false;
try
{
while (_pendingEffects.TryDequeue(out var effect))
{
if (_flushDepth++ > 1000)
{
_pendingEffects.Clear();
_pendingEffects.TrimExcess(16);
#if DEBUG
var exception = new InfiniteLoopException(_effectExecutions);
OnFlushInfiniteLoop?.Invoke(exception);
throw exception;
#else
throw new InfiniteLoopException();
#endif
}
if (effect.ShouldRun)
{
effect.Run();
#if DEBUG
_effectExecutions[effect] = _effectExecutions.GetValueOrDefault(effect) + 1;
#endif
}
}
}
finally
{
_flushDepth = 0;
_isFlushing = false;
#if DEBUG
_effectExecutions.Clear();
#endif
}
}
#if DEBUG
/// <summary>
/// Which effects have executing during the current flush, and how many times they've executed.
/// </summary>
private readonly Dictionary<Effect, int> _effectExecutions = [];
/// <summary>
/// Called when an infinite loop occurred during a flush.
/// </summary>
public static event Action<InfiniteLoopException>? OnFlushInfiniteLoop;
#endif
}
#if SANDBOX
using System.Diagnostics;
using Sandbox.Reactivity.Internals;
using Sandbox.UI;
using static Sandbox.Reactivity.Reactive;
#if JETBRAINS_ANNOTATIONS
using JetBrains.Annotations;
#endif
namespace Sandbox.Reactivity;
/// <summary>
/// The reactive counterpart to <see cref="Panel" /> that allows usage of reactive properties.
/// </summary>
/// <remarks>
/// Make sure you set up an effect root using <see cref="PanelRoot" /> at the top of your razor markup:
/// <code>
/// @{ using var _ = PanelRoot(); }
/// </code>
/// Engine limitations prevent this from being done automatically.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
[PublicAPI]
#endif
public class ReactivePanel : Panel, IReactivePropertyContainer, IReactivePanel
{
private Effect? _effectRoot;
private Effect? _renderEffectRoot;
private int _version;
public ReactivePanel()
{
var parent = Runtime.CurrentEffect;
_effectRoot = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
{
OnActivate();
return null;
},
parent,
false);
_effectRoot.SetDebugInfo(DisplayInfo.For(this).Name,
DisplayInfo.For(this).Icon,
new CallLocation(GetType(), nameof(OnActivate)),
parent ?? (object?)this);
_effectRoot.Run();
}
Effect? IReactivePanel.RenderEffectRoot
{
get => _renderEffectRoot;
set => _renderEffectRoot = value;
}
int IReactivePanel.Version
{
get => _version;
set => _version = value;
}
Dictionary<int, IProducer> IReactivePropertyContainer.Producers { get; } = [];
protected ReactivePanelScope PanelRoot()
{
return new ReactivePanelScope(this);
}
public sealed override void Delete(bool immediate = false)
{
_renderEffectRoot?.Dispose();
_renderEffectRoot = null;
_effectRoot?.Dispose();
_effectRoot = null;
base.Delete(immediate);
}
protected sealed override int BuildHash()
{
return _version;
}
/// <summary>
/// Called inside an effect root when this panel is instantiated, allowing for effects to be created. When this
/// panel is deleted, the effect root (and all of its descendants) are disposed.
/// </summary>
protected virtual void OnActivate()
{
}
}
#endif
#if SANDBOX
using Sandbox.Reactivity.Internals;
#if JETBRAINS_ANNOTATIONS
using JetBrains.Annotations;
#endif
// we can't wrap the BuildRenderTree method for razor components, so we need something that can set up the proper
// scope inside the markup itself
namespace Sandbox.Reactivity;
/// <summary>
/// A disposable that's used to enable reactivity for a <see cref="ReactivePanelComponent" /> or
/// <see cref="ReactivePanel" /> during rendering.
/// </summary>
#if JETBRAINS_ANNOTATIONS
[PublicAPI]
#endif
public readonly ref struct ReactivePanelScope : IDisposable
{
private readonly Effect.ExecutionScope _executionScope;
internal ReactivePanelScope(IReactivePanel panel)
{
if (panel.RenderEffectRoot is { } previousRoot)
{
// don't teardown previous root since we're already building the render tree by this point
previousRoot.Dispose(false);
}
// nested panels don't render immediately when a containing panel's tree is rendering, so the parent is
// always null anyway
var effectRoot = new Effect(null, null, true, () => panel.Version++);
effectRoot.SetDebugInfo(panel.GetType().ToSimpleString(false) + " (Render)",
panel is ReactivePanel ? "view_quilt" : "monitor",
new CallLocation(2),
panel is ReactivePanel reactive ? reactive.GameObject?.GetComponent<IReactivePanel>() : panel);
panel.RenderEffectRoot = effectRoot;
_executionScope = new Effect.ExecutionScope(effectRoot);
}
public void Dispose()
{
_executionScope.Dispose();
}
}
#endif
#if SANDBOX
namespace Sandbox.Reactivity.Internals;
/// <summary>
/// Maintains a list of types that are assignable to the given type.
/// </summary>
internal static class TypeHierarchy<T>
{
/// <summary>
/// All types that are assignable to <typeparamref name="T"/>.
/// </summary>
[SkipHotload]
// ReSharper disable once StaticMemberInGenericType
public static readonly IEnumerable<Type> Types;
static TypeHierarchy()
{
// since this is most likely going to be used for simple event types, we're going to assume that the hierarchy
// won't be very large and that checking a list would be faster than hashing for a set
var next = typeof(T);
var hierarchy = new List<Type>();
while (next != null)
{
hierarchy.Add(next);
foreach (var type in next.GetInterfaces())
{
if (!hierarchy.Contains(type))
{
hierarchy.Add(type);
}
}
next = next.BaseType;
if (next == typeof(object))
{
break;
}
}
Types = hierarchy;
}
}
#endif
#if JETBRAINS_ANNOTATIONS
#endif
namespace Sandbox.Reactivity;
/// <summary>
/// An object that contains a reactive value. Reading the value inside an effect will cause it to re-run when it
/// changes.
/// </summary>
/// <typeparam name="T">The type of value this object contains.</typeparam>
/// <remarks>
/// This can be used to abstract over a <see cref="State{T}" /> or <see cref="Derived{T}" /> as needed.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
#endif
public interface IState<T>
{
/// <summary>
/// The current value.
/// </summary>
T Value { get; set; }
}
/// <inheritdoc cref="IState{T}" />
#if JETBRAINS_ANNOTATIONS
#endif
public interface IReadOnlyState<out T>
{
/// <inheritdoc cref="IState{T}.Value" />
T Value { get; }
}
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" );
}
}
[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
}
}
/// <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;
}
}
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();
}
}
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; }
}
}
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() );
}
}
}
}
/// <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;
}
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;
}
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();
}
}