using System; using System.Collections.Generic; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Shared.Animations; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Robust.Client.GameObjects { [RegisterComponent] [ComponentReference(typeof(IPointLightComponent))] public class PointLightComponent : Component, IPointLightComponent, ISerializationHooks { [Dependency] private readonly IResourceCache _resourceCache = default!; public override string Name => "PointLight"; public override uint? NetID => NetIDs.POINT_LIGHT; internal bool TreeUpdateQueued { get; set; } [ViewVariables(VVAccess.ReadWrite)] [Animatable] public Color Color { get => _color; set => _color = value; } [ViewVariables(VVAccess.ReadWrite)] public Vector2 Offset { get => _offset; set => _offset = value; } [ViewVariables(VVAccess.ReadWrite)] [Animatable] public bool Enabled { get => _enabled; set { if (_enabled == value) return; _enabled = value; Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PointLightUpdateEvent()); } } [ViewVariables(VVAccess.ReadWrite)] public bool ContainerOccluded { get => _containerOccluded; set { if (_containerOccluded == value) return; _containerOccluded = value; Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new PointLightUpdateEvent()); } } private bool _containerOccluded; /// /// Determines if the light mask should automatically rotate with the entity. (like a flashlight) /// [ViewVariables(VVAccess.ReadWrite)] public bool MaskAutoRotate { get => _maskAutoRotate; set => _maskAutoRotate = value; } /// /// Local rotation of the light mask around the center origin /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public Angle Rotation { get => _rotation; set => _rotation = value; } /// /// /// The resource path to the mask texture the light will use. /// [ViewVariables(VVAccess.ReadWrite)] public string? MaskPath { get => _maskPath; set { _maskPath = value; UpdateMask(); } } /// /// Set a mask texture that will be applied to the light while rendering. /// The mask's red channel will be linearly multiplied.p /// [ViewVariables(VVAccess.ReadWrite)] public Texture? Mask { get; set; } [ViewVariables(VVAccess.ReadWrite)] [Animatable] public float Energy { get => _energy; set => _energy = value; } /// /// Soft shadow strength multiplier. /// Has no effect if soft shadows are not enabled. /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public float Softness { get => _softness; set => _softness = value; } [ViewVariables(VVAccess.ReadWrite)] public bool VisibleNested { get => _visibleNested; set { if (_visibleNested == value) return; _visibleNested = value; if (value) { if (Owner.Transform.Parent == null) return; _lightOnParent = true; } else { if (!_lightOnParent) return; _lightOnParent = false; } } } [DataField("radius")] private float _radius = 5f; [DataField("nestedvisible")] private bool _visibleNested = true; private bool _lightOnParent; [DataField("color")] private Color _color = Color.White; [DataField("offset")] private Vector2 _offset = Vector2.Zero; [DataField("enabled")] private bool _enabled = true; [DataField("autoRot")] private bool _maskAutoRotate; private Angle _rotation; [DataField("energy")] private float _energy = 1f; [DataField("softness")] private float _softness = 1f; [DataField("mask")] private string? _maskPath; /// /// Radius, in meters. /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public float Radius { get => _radius; set { _radius = MathF.Max(value, 0.01f); // setting radius to 0 causes exceptions, so just use a value close enough to zero that it's unnoticeable. Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new PointLightRadiusChangedEvent(this)); } } private void UpdateMask() { if (_maskPath is not null) Mask = _resourceCache.GetResource(_maskPath); else Mask = null; } /// /// What MapId we are intersecting for RenderingTreeSystem. /// [ViewVariables] internal MapId IntersectingMapId { get; set; } = MapId.Nullspace; /// /// What grids we're on for RenderingTreeSystem. /// [ViewVariables] internal List IntersectingGrids = new(); void ISerializationHooks.AfterDeserialization() { if (_maskPath != null) { Mask = IoCManager.Resolve().GetResource(_maskPath); } } public override void Initialize() { base.Initialize(); UpdateMask(); } /// public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); if (message is ParentChangedMessage msg) { HandleTransformParentChanged(msg); } } private void HandleTransformParentChanged(ParentChangedMessage obj) { // TODO: this does not work for things nested multiply layers deep. if (!VisibleNested) { return; } if (obj.NewParent != null && obj.NewParent.IsValid()) { _lightOnParent = true; } else { _lightOnParent = false; } } public override void OnRemove() { base.OnRemove(); var map = Owner.Transform.MapID; if (map != MapId.Nullspace) { Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new RenderTreeRemoveLightEvent(this, map)); } } /// public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { if (curState == null) return; var newState = (PointLightComponentState) curState; Enabled = newState.Enabled; Radius = newState.Radius; Offset = newState.Offset; Color = newState.Color; } } public class PointLightRadiusChangedEvent : EntityEventArgs { public PointLightComponent PointLightComponent { get; } public PointLightRadiusChangedEvent(PointLightComponent pointLightComponent) { PointLightComponent = pointLightComponent; } } internal sealed class PointLightUpdateEvent : EntityEventArgs { } }