From 86c318cc74779b5e760b0ad39b670b0070308318 Mon Sep 17 00:00:00 2001 From: "R. Neuser" Date: Sun, 19 Jul 2020 10:32:26 -0500 Subject: [PATCH] OverlayManager 2 Electric Boogaloo (#1410) --- .../Mobs/ClientOverlayEffectsComponent.cs | 111 ++++++++++++------ .../Graphics/Overlays/CircleMaskOverlay.cs | 2 +- .../Graphics/Overlays/FlashOverlay.cs | 6 +- .../Graphics/Overlays/GradientCircleMask.cs | 2 +- .../DamageThresholdTemplates/HumanTemplate.cs | 12 +- .../Mobs/ServerOverlayEffectsComponent.cs | 68 ++++++++--- .../Components/Weapon/Melee/FlashComponent.cs | 28 +++-- .../TimedOverlayRemovalSystem.cs | 45 +++++++ .../Mobs/SharedOverlayEffectsComponent.cs | 86 ++++++++++---- .../Entities/Items/Weapons/security.yml | 1 - 10 files changed, 264 insertions(+), 97 deletions(-) create mode 100644 Content.Server/GameObjects/EntitySystems/TimedOverlayRemovalSystem.cs diff --git a/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs b/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs index acd8928375..46d5eb5872 100644 --- a/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs @@ -10,9 +10,11 @@ using Robust.Client.Interfaces.Graphics.Overlays; using Robust.Client.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Reflection; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Players; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -26,7 +28,7 @@ namespace Content.Client.GameObjects.Components.Mobs public sealed class ClientOverlayEffectsComponent : SharedOverlayEffectsComponent//, ICharacterUI { /// - /// An enum representing the current state being applied to the user + /// A list of overlay containers representing the current overlays applied /// private List _currentEffects = new List(); @@ -41,43 +43,57 @@ namespace Content.Client.GameObjects.Components.Mobs // Required dependencies [Dependency] private readonly IOverlayManager _overlayManager; [Dependency] private readonly IReflectionManager _reflectionManager; - [Dependency] private readonly IPlayerManager _playerManager; + [Dependency] private readonly IClientNetManager _netManager; #pragma warning restore 649 + public override void Initialize() + { + base.Initialize(); + + UpdateOverlays(); + } + public override void HandleMessage(ComponentMessage message, IComponent component) { switch (message) { case PlayerAttachedMsg _: - var overlays = new List(_currentEffects); - _currentEffects.Clear(); - SetEffects(overlays); + UpdateOverlays(); break; case PlayerDetachedMsg _: - ActiveOverlays = new List(); + ActiveOverlays.ForEach(o => _overlayManager.RemoveOverlay(o.ID)); break; } } - public override void HandleComponentState(ComponentState curState, ComponentState nextState) + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null) { - base.HandleComponentState(curState, nextState); - - if (!(curState is OverlayEffectComponentState state)) + base.HandleNetworkMessage(message, netChannel, session); + if (message is OverlayEffectComponentMessage overlayMessage) { - return; + SetEffects(overlayMessage.Overlays); + } + } + + private void UpdateOverlays() + { + _currentEffects = _overlayManager.AllOverlays + .Where(overlay => Enum.IsDefined(typeof(SharedOverlayID), overlay.ID)) + .Select(overlay => new OverlayContainer(overlay.ID)) + .ToList(); + + foreach (var overlayContainer in ActiveOverlays) + { + if (!_overlayManager.HasOverlay(overlayContainer.ID)) + { + if (TryCreateOverlay(overlayContainer, out var overlay)) + { + _overlayManager.AddOverlay(overlay); + } + } } - if (_playerManager?.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity != Owner) - { - _currentEffects = state.Overlays; - return; - } - - if (ActiveOverlays.Equals(state.Overlays)) - return; - - ActiveOverlays = state.Overlays; + SendNetworkMessage(new ResendOverlaysMessage(), _netManager.ServerChannel); } private void SetEffects(List newOverlays) @@ -96,7 +112,13 @@ namespace Content.Client.GameObjects.Components.Mobs { AddOverlay(container); } + else + { + UpdateOverlayConfiguration(container, _overlayManager.GetOverlay(container.ID)); + } } + + _currentEffects = newOverlays; } private void RemoveOverlay(OverlayContainer container) @@ -107,6 +129,11 @@ namespace Content.Client.GameObjects.Components.Mobs private void AddOverlay(OverlayContainer container) { + if (_overlayManager.HasOverlay(container.ID)) + { + return; + } + ActiveOverlays.Add(container); if (TryCreateOverlay(container, out var overlay)) { @@ -118,26 +145,38 @@ namespace Content.Client.GameObjects.Components.Mobs } } + private void UpdateOverlayConfiguration(OverlayContainer container, Overlay overlay) + { + var configurableTypes = overlay.GetType() + .GetInterfaces() + .Where(type => + type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(IConfigurable<>) + && container.Parameters.Exists(p => p.GetType() == type.GenericTypeArguments.First())) + .ToList(); + + if (configurableTypes.Count > 0) + { + foreach (var type in configurableTypes) + { + var method = type.GetMethod(nameof(IConfigurable.Configure)); + var parameter = container.Parameters + .First(p => p.GetType() == type.GenericTypeArguments.First()); + + method!.Invoke(overlay, new []{ parameter }); + } + } + } + private bool TryCreateOverlay(OverlayContainer container, out Overlay overlay) { var overlayTypes = _reflectionManager.GetAllChildren(); - var foundType = overlayTypes.FirstOrDefault(t => t.Name == container.ID); + var overlayType = overlayTypes.FirstOrDefault(t => t.Name == container.ID); - if (foundType != null) + if (overlayType != null) { - overlay = Activator.CreateInstance(foundType) as Overlay; - var configurable = foundType - .GetInterfaces() - .FirstOrDefault(type => - type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IConfigurable<>) - && type.GenericTypeArguments.First() == container.GetType()); - - if (configurable != null) - { - var method = overlay?.GetType().GetMethod("Configure"); - method?.Invoke(overlay, new []{ container }); - } - + overlay = Activator.CreateInstance(overlayType) as Overlay; + UpdateOverlayConfiguration(container, overlay); return true; } diff --git a/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs b/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs index ef6832480a..ba4942d83b 100644 --- a/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs +++ b/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs @@ -18,7 +18,7 @@ namespace Content.Client.Graphics.Overlays public override OverlaySpace Space => OverlaySpace.WorldSpace; - public CircleMaskOverlay() : base(nameof(OverlayType.CircleMaskOverlay)) + public CircleMaskOverlay() : base(nameof(SharedOverlayID.CircleMaskOverlay)) { IoCManager.InjectDependencies(this); Shader = _prototypeManager.Index("CircleMask").Instance(); diff --git a/Content.Client/Graphics/Overlays/FlashOverlay.cs b/Content.Client/Graphics/Overlays/FlashOverlay.cs index f156225434..5ef3884d15 100644 --- a/Content.Client/Graphics/Overlays/FlashOverlay.cs +++ b/Content.Client/Graphics/Overlays/FlashOverlay.cs @@ -18,7 +18,7 @@ using Color = Robust.Shared.Maths.Color; namespace Content.Client.Graphics.Overlays { - public class FlashOverlay : Overlay, IConfigurable + public class FlashOverlay : Overlay, IConfigurable { #pragma warning disable 649 [Dependency] private readonly IPrototypeManager _prototypeManager; @@ -31,7 +31,7 @@ namespace Content.Client.Graphics.Overlays private int lastsFor = 5000; private Texture _screenshotTexture; - public FlashOverlay() : base(nameof(OverlayType.FlashOverlay)) + public FlashOverlay() : base(nameof(SharedOverlayID.FlashOverlay)) { IoCManager.InjectDependencies(this); Shader = _prototypeManager.Index("FlashedEffect").Instance().Duplicate(); @@ -65,7 +65,7 @@ namespace Content.Client.Graphics.Overlays _screenshotTexture = null; } - public void Configure(TimedOverlayContainer parameters) + public void Configure(TimedOverlayParameter parameters) { lastsFor = parameters.Length; } diff --git a/Content.Client/Graphics/Overlays/GradientCircleMask.cs b/Content.Client/Graphics/Overlays/GradientCircleMask.cs index e95fe30d83..315fb592f9 100644 --- a/Content.Client/Graphics/Overlays/GradientCircleMask.cs +++ b/Content.Client/Graphics/Overlays/GradientCircleMask.cs @@ -17,7 +17,7 @@ namespace Content.Client.Graphics.Overlays #pragma warning restore 649 public override OverlaySpace Space => OverlaySpace.WorldSpace; - public GradientCircleMaskOverlay() : base(nameof(OverlayType.GradientCircleMaskOverlay)) + public GradientCircleMaskOverlay() : base(nameof(SharedOverlayID.GradientCircleMaskOverlay)) { IoCManager.InjectDependencies(this); Shader = _prototypeManager.Index("GradientCircleMask").Instance(); diff --git a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs index 3b34bc892b..c5ec0e5f8e 100644 --- a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs +++ b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs @@ -69,24 +69,24 @@ namespace Content.Server.GameObjects statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Health, "/Textures/Interface/StatusEffects/Human/human" + modifier + ".png"); - overlayComponent?.RemoveOverlay(OverlayType.GradientCircleMaskOverlay); - overlayComponent?.RemoveOverlay(OverlayType.CircleMaskOverlay); + overlayComponent?.RemoveOverlay(SharedOverlayID.GradientCircleMaskOverlay); + overlayComponent?.RemoveOverlay(SharedOverlayID.CircleMaskOverlay); return; case ThresholdType.Critical: statusEffectsComponent?.ChangeStatusEffectIcon( StatusEffect.Health, "/Textures/Interface/StatusEffects/Human/humancrit-0.png"); - overlayComponent?.ClearOverlays(); - overlayComponent?.AddOverlay(OverlayType.GradientCircleMaskOverlay); + overlayComponent?.AddOverlay(SharedOverlayID.GradientCircleMaskOverlay); + overlayComponent?.RemoveOverlay(SharedOverlayID.CircleMaskOverlay); return; case ThresholdType.Death: statusEffectsComponent?.ChangeStatusEffectIcon( StatusEffect.Health, "/Textures/Interface/StatusEffects/Human/humandead.png"); - overlayComponent?.ClearOverlays(); - overlayComponent?.AddOverlay(OverlayType.CircleMaskOverlay); + overlayComponent?.RemoveOverlay(SharedOverlayID.GradientCircleMaskOverlay); + overlayComponent?.AddOverlay(SharedOverlayID.CircleMaskOverlay); return; default: diff --git a/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs index 92c0b2d5d5..edc74e61cd 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs @@ -2,7 +2,12 @@ using System; using System.Collections.Generic; using System.Linq; using Content.Shared.GameObjects.Components.Mobs; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Enums; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Players; using Robust.Shared.Timers; using Robust.Shared.ViewVariables; @@ -12,50 +17,83 @@ namespace Content.Server.GameObjects.Components.Mobs [ComponentReference(typeof(SharedOverlayEffectsComponent))] public sealed class ServerOverlayEffectsComponent : SharedOverlayEffectsComponent { - private readonly List _currentOverlays = new List(); + public ServerOverlayEffectsComponent() + { + NetSyncEnabled = false; + } [ViewVariables(VVAccess.ReadWrite)] - private List ActiveOverlays => _currentOverlays; + public List ActiveOverlays { get; } = new List(); - public override ComponentState GetComponentState() + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null) { - return new OverlayEffectComponentState(_currentOverlays); + if (Owner.TryGetComponent(out IActorComponent actor) && message is ResendOverlaysMessage) + { + if (actor.playerSession.ConnectedClient == netChannel) + { + SyncClient(); + } + } } + public void AddOverlay(string id) => AddOverlay(new OverlayContainer(id)); + + public void AddOverlay(SharedOverlayID id) => AddOverlay(new OverlayContainer(id)); + public void AddOverlay(OverlayContainer container) { if (!ActiveOverlays.Contains(container)) { ActiveOverlays.Add(container); - Dirty(); + SyncClient(); } } - public void AddOverlay(string id) => AddOverlay(new OverlayContainer(id)); - public void AddOverlay(OverlayType type) => AddOverlay(new OverlayContainer(type)); + public void RemoveOverlay(SharedOverlayID id) => RemoveOverlay(id.ToString()); + + public void RemoveOverlay(string id) => RemoveOverlay(new OverlayContainer(id)); public void RemoveOverlay(OverlayContainer container) { - if (ActiveOverlays.RemoveAll(c => c.Equals(container)) > 0) + if (ActiveOverlays.Remove(container)) { - Dirty(); + SyncClient(); } } - public void RemoveOverlay(string id) + public bool TryModifyOverlay(string id, Action modifications) { - if (ActiveOverlays.RemoveAll(container => container.ID == id) > 0) + var overlay = ActiveOverlays.Find(c => c.ID == id); + if (overlay == null) { - Dirty(); + return false; } - } - public void RemoveOverlay(OverlayType type) => RemoveOverlay(type.ToString()); + modifications(overlay); + SyncClient(); + return true; + } public void ClearOverlays() { + if (ActiveOverlays.Count == 0) + { + return; + } + ActiveOverlays.Clear(); - Dirty(); + SyncClient(); + } + + private void SyncClient() + { + if (Owner.TryGetComponent(out IActorComponent actor)) + { + if (actor.playerSession.ConnectedClient.IsConnected) + { + SendNetworkMessage(new OverlayEffectComponentMessage(ActiveOverlays), actor.playerSession.ConnectedClient); + } + } } } } diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs index d60085bb80..9f7da86092 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs @@ -31,7 +31,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee public override string Name => "Flash"; [ViewVariables(VVAccess.ReadWrite)] private int _flashDuration = 5000; - [ViewVariables(VVAccess.ReadWrite)] private float _flashFalloffExp = 8f; [ViewVariables(VVAccess.ReadWrite)] private int _uses = 5; [ViewVariables(VVAccess.ReadWrite)] private float _range = 3f; [ViewVariables(VVAccess.ReadWrite)] private int _aoeFlashDuration = 5000 / 3; @@ -55,9 +54,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee base.ExposeData(serializer); serializer.DataField(ref _flashDuration, "duration", 5000); - serializer.DataField(ref _flashFalloffExp, "flashFalloffExp", 8f); serializer.DataField(ref _uses, "uses", 5); - serializer.DataField(ref _range, "range", 3f); + serializer.DataField(ref _range, "range", 7f); serializer.DataField(ref _aoeFlashDuration, "aoeFlashDuration", _flashDuration / 3); serializer.DataField(ref _slowTo, "slowTo", 0.75f); } @@ -139,9 +137,18 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee { if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent)) { - var container = new TimedOverlayContainer(nameof(OverlayType.FlashOverlay), flashDuration); - overlayEffectsComponent.AddOverlay(container); - container.StartTimer(() => overlayEffectsComponent.RemoveOverlay(container)); + if (!overlayEffectsComponent.TryModifyOverlay(nameof(SharedOverlayID.FlashOverlay), + overlay => + { + if (overlay.TryGetOverlayParameter(out var timed)) + { + timed.Length += flashDuration; + } + })) + { + var container = new OverlayContainer(SharedOverlayID.FlashOverlay, new TimedOverlayParameter(flashDuration)); + overlayEffectsComponent.AddOverlay(container); + } } if (entity.TryGetComponent(out StunnableComponent stunnableComponent)) @@ -165,8 +172,13 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee if (inDetailsRange) { - message.AddMarkup(_localizationManager.GetString( - "The flash has [color=green]{0}[/color] uses remaining.", Uses)); + message.AddMarkup( + _localizationManager.GetString( + "The flash has [color=green]{0}[/color] {1} remaining.", + Uses, + _localizationManager.GetPluralString("use", "uses", Uses) + ) + ); } } } diff --git a/Content.Server/GameObjects/EntitySystems/TimedOverlayRemovalSystem.cs b/Content.Server/GameObjects/EntitySystems/TimedOverlayRemovalSystem.cs new file mode 100644 index 0000000000..ceb07b97b1 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/TimedOverlayRemovalSystem.cs @@ -0,0 +1,45 @@ +using Content.Server.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Mobs; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class TimedOverlayRemovalSystem : EntitySystem + { +#pragma warning disable 649 + [Dependency] private readonly IGameTiming _gameTiming; +#pragma warning restore 649 + + public override void Initialize() + { + base.Initialize(); + + EntityQuery = new TypeEntityQuery(typeof(ServerOverlayEffectsComponent)); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var entity in RelevantEntities) + { + var effectsComponent = entity.GetComponent(); + foreach (var overlay in effectsComponent.ActiveOverlays.ToArray()) + { + if (overlay.TryGetOverlayParameter(out var parameter)) + { + if (parameter.StartedAt + parameter.Length <= _gameTiming.CurTime.TotalMilliseconds) + { + effectsComponent.RemoveOverlay(overlay); + } + } + } + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs index fd68608336..e487f8940c 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading; +using System.Timers; using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; -using Robust.Shared.Timers; using Robust.Shared.ViewVariables; using YamlDotNet.RepresentationModel; using Component = Robust.Shared.GameObjects.Component; @@ -18,72 +23,101 @@ namespace Content.Shared.GameObjects.Components.Mobs public abstract class SharedOverlayEffectsComponent : Component { public override string Name => "OverlayEffectsUI"; + public sealed override uint? NetID => ContentNetIDs.OVERLAYEFFECTS; } [Serializable, NetSerializable] - public class OverlayContainer + public class OverlayContainer : IEquatable, IEquatable { [ViewVariables(VVAccess.ReadOnly)] public string ID { get; } + [ViewVariables(VVAccess.ReadWrite)] + public List Parameters { get; } = new List(); + public OverlayContainer([NotNull] string id) { ID = id; } - public OverlayContainer(OverlayType type) : this(type.ToString()) + public OverlayContainer(SharedOverlayID id) : this(id.ToString()) { - } - public override bool Equals(object obj) + public OverlayContainer(SharedOverlayID id, params OverlayParameter[] parameters) : this(id) { - if (obj is OverlayContainer container) + Parameters.AddRange(parameters); + } + + public bool TryGetOverlayParameter(out T parameter) where T : OverlayParameter + { + var found = Parameters.FirstOrDefault(p => p is T); + if (found != null) { - return container.ID == ID; + parameter = found as T; + return true; } - if (obj is string idString) - { - return idString == ID; - } + parameter = default; + return false; + } - return base.Equals(obj); + public bool Equals(string other) + { + return ID == other; + } + + public bool Equals(OverlayContainer other) + { + return ID == other?.ID; } public override int GetHashCode() { - return (ID != null ? ID.GetHashCode() : 0); + return ID != null ? ID.GetHashCode() : 0; } + } [Serializable, NetSerializable] - public class OverlayEffectComponentState : ComponentState + public class OverlayEffectComponentMessage : ComponentMessage { public List Overlays; - public OverlayEffectComponentState(List overlays) : base(ContentNetIDs.OVERLAYEFFECTS) + public OverlayEffectComponentMessage(List overlays) { + Directed = true; Overlays = overlays; } } [Serializable, NetSerializable] - public class TimedOverlayContainer : OverlayContainer + public class ResendOverlaysMessage : ComponentMessage { - [ViewVariables(VVAccess.ReadOnly)] - public int Length { get; } - - public TimedOverlayContainer(string id, int length) : base(id) - { - Length = length; - } - - public void StartTimer(Action finished) => Timer.Spawn(Length, finished); } - public enum OverlayType + [Serializable, NetSerializable] + public abstract class OverlayParameter + { + } + + [Serializable, NetSerializable] + public class TimedOverlayParameter : OverlayParameter + { + [ViewVariables(VVAccess.ReadOnly)] + public int Length { get; set; } + + public double StartedAt { get; private set; } + + public TimedOverlayParameter(int length) + { + Length = length; + StartedAt = IoCManager.Resolve().CurTime.TotalMilliseconds; + } + } + + public enum SharedOverlayID { GradientCircleMaskOverlay, CircleMaskOverlay, diff --git a/Resources/Prototypes/Entities/Items/Weapons/security.yml b/Resources/Prototypes/Entities/Items/Weapons/security.yml index 499a8d8f1b..b38f80b5d7 100644 --- a/Resources/Prototypes/Entities/Items/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Items/Weapons/security.yml @@ -42,7 +42,6 @@ cooldownTime: 1 arc: smash hitSound: /Audio/Weapons/flash.ogg - slowTo: 0.7 - type: Item Size: 2