]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict and cleanup RingerComponent (#35907)
authorMilon <milonpl.git@proton.me>
Sat, 19 Apr 2025 13:55:05 +0000 (15:55 +0200)
committerGitHub <noreply@github.com>
Sat, 19 Apr 2025 13:55:05 +0000 (15:55 +0200)
* clean up most stuff

* move to shared

* works

* shuffle shit around

* oops! access

* fixes

* todo: everything

* SUFFERING

* curse you

18 files changed:
Content.Client/PDA/Ringer/RingerBoundUserInterface.cs
Content.Client/PDA/Ringer/RingerSystem.cs [new file with mode: 0644]
Content.Client/PDA/Ringer/RingtoneMenu.xaml
Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Content.Server/PDA/PdaSystem.cs
Content.Server/PDA/Ringer/RingerComponent.cs [deleted file]
Content.Server/PDA/Ringer/RingerSystem.cs
Content.Server/PDA/Ringer/RingerUplinkComponent.cs [deleted file]
Content.Server/Store/Systems/StoreSystem.Ui.cs
Content.Shared/PDA/Ringer/RingerComponent.cs [new file with mode: 0644]
Content.Shared/PDA/Ringer/RingerMessagesUI.cs
Content.Shared/PDA/Ringer/RingerUpdateState.cs [deleted file]
Content.Shared/PDA/Ringer/RingerUplinkComponent.cs [new file with mode: 0644]
Content.Shared/PDA/Ringer/RingerVisuals.cs
Content.Shared/PDA/SharedPdaSystem.cs
Content.Shared/PDA/SharedRingerSystem.cs
Resources/Prototypes/Entities/Objects/Devices/pda.yml

index 170a296ac2e2067f1d2f14cc7d16fb0e1334707a..fc778de7d9dc56595d26049c62bb26827a924e50 100644 (file)
@@ -7,40 +7,21 @@ using Robust.Shared.Timing;
 namespace Content.Client.PDA.Ringer
 {
     [UsedImplicitly]
-    public sealed class RingerBoundUserInterface : BoundUserInterface
+    public sealed class RingerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
     {
         [ViewVariables]
         private RingtoneMenu? _menu;
 
-        public RingerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
-        {
-        }
-
         protected override void Open()
         {
             base.Open();
             _menu = this.CreateWindow<RingtoneMenu>();
             _menu.OpenToLeft();
 
-            _menu.TestRingerButton.OnPressed += _ =>
-            {
-                SendMessage(new RingerPlayRingtoneMessage());
-            };
-
-            _menu.SetRingerButton.OnPressed += _ =>
-            {
-                if (!TryGetRingtone(out var ringtone))
-                    return;
-
-                SendMessage(new RingerSetRingtoneMessage(ringtone));
-                _menu.SetRingerButton.Disabled = true;
+            _menu.TestRingtoneButtonPressed += OnTestRingtoneButtonPressed;
+            _menu.SetRingtoneButtonPressed += OnSetRingtoneButtonPressed;
 
-                Timer.Spawn(333, () =>
-                {
-                    if (_menu is { Disposed: false, SetRingerButton: { Disposed: false } ringer})
-                        ringer.Disabled = false;
-                });
-            };
+            Update();
         }
 
         private bool TryGetRingtone(out Note[] ringtone)
@@ -63,36 +44,59 @@ namespace Content.Client.PDA.Ringer
             return true;
         }
 
-        protected override void UpdateState(BoundUserInterfaceState state)
+        public override void Update()
         {
-            base.UpdateState(state);
+            base.Update();
 
-            if (_menu == null || state is not RingerUpdateState msg)
+            if (_menu == null)
                 return;
 
-            for (int i = 0; i < _menu.RingerNoteInputs.Length; i++)
+            if (!EntMan.TryGetComponent(Owner, out RingerComponent? ringer))
+                return;
+
+            for (var i = 0; i < _menu.RingerNoteInputs.Length; i++)
             {
+                var note = ringer.Ringtone[i].ToString();
 
-                var note = msg.Ringtone[i].ToString();
-                if (RingtoneMenu.IsNote(note))
-                {
-                    _menu.PreviousNoteInputs[i] = note.Replace("sharp", "#");
-                    _menu.RingerNoteInputs[i].Text = _menu.PreviousNoteInputs[i];
-                }
+                if (!RingtoneMenu.IsNote(note))
+                    continue;
 
+                _menu.PreviousNoteInputs[i] = note.Replace("sharp", "#");
+                _menu.RingerNoteInputs[i].Text = _menu.PreviousNoteInputs[i];
             }
 
-            _menu.TestRingerButton.Disabled = msg.IsPlaying;
+            _menu.TestRingerButton.Disabled = ringer.Active;
         }
 
+        private void OnTestRingtoneButtonPressed()
+        {
+            if (_menu is null)
+                return;
+
+            SendPredictedMessage(new RingerPlayRingtoneMessage());
+
+            // We disable it instantly to remove the delay before the client receives the next compstate
+            // Makes the UI feel responsive, will be re-enabled by ringer.Active once it gets an update.
+            _menu.TestRingerButton.Disabled = true;
+        }
 
-        protected override void Dispose(bool disposing)
+        private void OnSetRingtoneButtonPressed()
         {
-            base.Dispose(disposing);
-            if (!disposing)
+            if (_menu is null)
                 return;
 
-            _menu?.Dispose();
+            if (!TryGetRingtone(out var ringtone))
+                return;
+
+            SendPredictedMessage(new RingerSetRingtoneMessage(ringtone));
+            _menu.SetRingerButton.Disabled = true;
+
+            Timer.Spawn(333,
+                () =>
+                {
+                    if (_menu is { Disposed: false, SetRingerButton: { Disposed: false } ringer} )
+                        ringer.Disabled = false;
+                });
         }
     }
 }
diff --git a/Content.Client/PDA/Ringer/RingerSystem.cs b/Content.Client/PDA/Ringer/RingerSystem.cs
new file mode 100644 (file)
index 0000000..6d45a8c
--- /dev/null
@@ -0,0 +1,56 @@
+using Content.Shared.PDA;
+using Content.Shared.PDA.Ringer;
+using Content.Shared.Store.Components;
+
+namespace Content.Client.PDA.Ringer;
+
+/// <summary>
+/// Handles the client-side logic for <see cref="SharedRingerSystem"/>.
+/// </summary>
+public sealed class RingerSystem : SharedRingerSystem
+{
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RingerComponent, AfterAutoHandleStateEvent>(OnRingerUpdate);
+    }
+
+    /// <summary>
+    /// Updates the UI whenever we get a new component state from the server.
+    /// </summary>
+    private void OnRingerUpdate(Entity<RingerComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        UpdateRingerUi(ent);
+    }
+
+    /// <inheritdoc/>
+    protected override void UpdateRingerUi(Entity<RingerComponent> ent)
+    {
+        if (UI.TryGetOpenUi(ent.Owner, RingerUiKey.Key, out var bui))
+        {
+            bui.Update();
+        }
+    }
+
+    /// <inheritdoc/>
+    public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null)
+    {
+        if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
+            return false;
+
+        if (!HasComp<StoreComponent>(uid))
+            return false;
+
+        // Special case for client-side prediction:
+        // Since we can't expose the uplink code to clients for security reasons,
+        // we assume if an antagonist is trying to set a ringtone, it's to unlock the uplink.
+        // The server will properly verify the code and correct if needed.
+        if (IsAntagonist(user))
+            return ToggleUplinkInternal((uid, uplink));
+
+        // Non-antagonists never get to toggle the uplink on the client
+        return false;
+    }
+}
index 2fff0ab1b560c077f1de20d6c65d05dba635d72a..1bef4d433f8ee70a6cb21c2a7fcbb4551af64724 100644 (file)
@@ -1,7 +1,8 @@
-<DefaultWindow xmlns="https://spacestation14.io"
-            Title="{Loc 'comp-ringer-ui-menu-title'}"
-            MinSize="320 128"
-            SetSize="320 128">
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                      xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                      Title="{Loc 'comp-ringer-ui-menu-title'}"
+            MinSize="320 100"
+            SetSize="320 100">
         <BoxContainer Orientation="Vertical"
                     VerticalExpand="True"
                     HorizontalExpand="True"
@@ -90,4 +91,4 @@
                 </BoxContainer>
             </PanelContainer>
         </BoxContainer>
-</DefaultWindow>
+</controls:FancyWindow>
index 044714b06fe64c51bf8978dc10dd08ba6d173224..989fe658430344211bc38f32a70a0dd30bab7e75 100644 (file)
@@ -1,6 +1,6 @@
 using System.Numerics;
+using Content.Client.UserInterface.Controls;
 using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
 using Content.Shared.PDA;
 using Robust.Client.UserInterface.Controls;
@@ -8,15 +8,21 @@ using Robust.Client.UserInterface.Controls;
 namespace Content.Client.PDA.Ringer
 {
     [GenerateTypedNameReferences]
-    public sealed partial class RingtoneMenu : DefaultWindow
+    public sealed partial class RingtoneMenu : FancyWindow
     {
         public string[] PreviousNoteInputs = new[] { "A", "A", "A", "A", "A", "A" };
-        public LineEdit[] RingerNoteInputs = default!;
+        public LineEdit[] RingerNoteInputs;
+
+        public event Action? SetRingtoneButtonPressed;
+        public event Action? TestRingtoneButtonPressed;
 
         public RingtoneMenu()
         {
             RobustXamlLoader.Load(this);
 
+            SetRingerButton.OnPressed += _ => SetRingtoneButtonPressed?.Invoke();
+            TestRingerButton.OnPressed += _ => TestRingtoneButtonPressed?.Invoke();
+
             RingerNoteInputs = new[] { RingerNoteOneInput, RingerNoteTwoInput, RingerNoteThreeInput, RingerNoteFourInput, RingerNoteFiveInput, RingerNoteSixInput };
 
             for (var i = 0; i < RingerNoteInputs.Length; ++i)
@@ -43,14 +49,28 @@ namespace Content.Client.PDA.Ringer
                     foo();
                     input.CursorPosition = input.Text.Length; // Resets caret position to the end of the typed input
                 };
-                input.OnTextChanged += _ =>
+
+                input.OnTextChanged += args =>
                 {
-                    input.Text = input.Text.ToUpper();
+                    // Convert to uppercase
+                    var upperText = args.Text.ToUpper();
 
-                    if (!IsNote(input.Text))
+                    // Filter to only valid notes
+                    var newText = upperText;
+                    if (!IsNote(newText))
+                    {
+                        newText = PreviousNoteInputs[index];
                         input.AddStyleClass("Caution");
+                    }
                     else
+                    {
+                        PreviousNoteInputs[index] = newText;
                         input.RemoveStyleClass("Caution");
+                    }
+
+                    // Only update if there's a change
+                    if (newText != input.Text)
+                        input.Text = newText;
                 };
             }
         }
index 4d041af3a0c234d5d824b420fc435c2cadfdf2a3..790b14579eb461ea89a207fde1137de71bad00e7 100644 (file)
@@ -184,13 +184,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         {
             Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA");
             // Codes are only generated if the uplink is a PDA
-            code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
+            var ev = new GenerateUplinkCodeEvent();
+            RaiseLocalEvent(pda.Value, ref ev);
 
-            // If giveUplink is false the uplink code part is omitted
-            briefing = string.Format("{0}\n{1}",
-                briefing,
-                Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
-            return (code, briefing);
+            if (ev.Code is { } generatedCode)
+            {
+                code = generatedCode;
+
+                // If giveUplink is false the uplink code part is omitted
+                briefing = string.Format("{0}\n{1}",
+                    briefing,
+                    Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
+                return (code, briefing);
+            }
         }
         else if (pda is null && uplinked)
         {
index bdf688efe733f3bf27f5c5a889a26240797bf643..bfa8e1825ddc8bfbec7b959bbed12148d52961fb 100644 (file)
@@ -10,15 +10,16 @@ using Content.Server.Traitor.Uplink;
 using Content.Shared.Access.Components;
 using Content.Shared.CartridgeLoader;
 using Content.Shared.Chat;
+using Content.Shared.DeviceNetwork.Components;
 using Content.Shared.Light;
 using Content.Shared.Light.EntitySystems;
 using Content.Shared.PDA;
+using Content.Shared.PDA.Ringer;
 using Robust.Server.Containers;
 using Robust.Server.GameObjects;
 using Robust.Shared.Containers;
 using Robust.Shared.Player;
 using Robust.Shared.Utility;
-using Content.Shared.DeviceNetwork.Components;
 
 namespace Content.Server.PDA
 {
@@ -166,7 +167,7 @@ namespace Content.Server.PDA
         /// <summary>
         /// Send new UI state to clients, call if you modify something like uplink.
         /// </summary>
-        public void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null)
+        public override void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null)
         {
             if (!Resolve(uid, ref pda, false))
                 return;
@@ -243,7 +244,7 @@ namespace Content.Server.PDA
                 return;
 
             if (HasComp<RingerComponent>(uid))
-                _ringer.ToggleRingerUI(uid, msg.Actor);
+                _ringer.TryToggleRingerUi(uid, msg.Actor);
         }
 
         private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowMusicMessage msg)
@@ -272,7 +273,7 @@ namespace Content.Server.PDA
 
             if (TryComp<RingerUplinkComponent>(uid, out var uplink))
             {
-                _ringer.LockUplink(uid, uplink);
+                _ringer.LockUplink((uid, uplink));
                 UpdatePdaUi(uid, pda);
             }
         }
diff --git a/Content.Server/PDA/Ringer/RingerComponent.cs b/Content.Server/PDA/Ringer/RingerComponent.cs
deleted file mode 100644 (file)
index 55dc458..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-using Content.Shared.PDA;
-
-namespace Content.Server.PDA.Ringer
-{
-    [RegisterComponent]
-    public sealed partial class RingerComponent : Component
-    {
-        [DataField("ringtone")]
-        public Note[] Ringtone = new Note[SharedRingerSystem.RingtoneLength];
-
-        [DataField("timeElapsed")]
-        public float TimeElapsed = 0;
-
-        /// <summary>
-        /// Keeps track of how many notes have elapsed if the ringer component is playing.
-        /// </summary>
-        [DataField("noteCount")]
-        public int NoteCount = 0;
-
-        /// <summary>
-        /// How far the sound projects in metres.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("range")]
-        public float Range = 3f;
-
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("volume")]
-        public float Volume = -4f;
-    }
-
-    [RegisterComponent]
-    public sealed partial class ActiveRingerComponent : Component
-    {
-    }
-}
index 0cc4ea86c2210b9790168854d7a27aa44cc5f1bf..dbdc5e83f3f8f2d8cd9762958bfe0f6774ffb774 100644 (file)
 using System.Linq;
-using System.Runtime.InteropServices;
-using Content.Server.Store.Components;
 using Content.Server.Store.Systems;
 using Content.Shared.PDA;
 using Content.Shared.PDA.Ringer;
-using Content.Shared.Popups;
-using Content.Shared.Store;
 using Content.Shared.Store.Components;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Network;
-using Robust.Shared.Player;
 using Robust.Shared.Random;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-using Robust.Server.Audio;
 
-namespace Content.Server.PDA.Ringer
-{
-    public sealed class RingerSystem : SharedRingerSystem
-    {
-        [Dependency] private readonly PdaSystem _pda = default!;
-        [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly IRobustRandom _random = default!;
-        [Dependency] private readonly UserInterfaceSystem _ui = default!;
-        [Dependency] private readonly AudioSystem _audio = default!;
-        [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
-        [Dependency] private readonly TransformSystem _transform = default!;
-
-        private readonly Dictionary<NetUserId, TimeSpan> _lastSetRingtoneAt = new();
-
-        public override void Initialize()
-        {
-            base.Initialize();
-
-            // General Event Subscriptions
-            SubscribeLocalEvent<RingerComponent, MapInitEvent>(RandomizeRingtone);
-            SubscribeLocalEvent<RingerUplinkComponent, ComponentInit>(RandomizeUplinkCode);
-            // RingerBoundUserInterface Subscriptions
-            SubscribeLocalEvent<RingerComponent, RingerSetRingtoneMessage>(OnSetRingtone);
-            SubscribeLocalEvent<RingerUplinkComponent, BeforeRingtoneSetEvent>(OnSetUplinkRingtone);
-            SubscribeLocalEvent<RingerComponent, RingerPlayRingtoneMessage>(RingerPlayRingtone);
-            SubscribeLocalEvent<RingerComponent, RingerRequestUpdateInterfaceMessage>(UpdateRingerUserInterfaceDriver);
-
-            SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
-        }
-
-        //Event Functions
-
-        private void OnCurrencyInsert(EntityUid uid, RingerComponent ringer, CurrencyInsertAttemptEvent args)
-        {
-            if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
-            {
-                args.Cancel();
-                return;
-            }
-
-            // if the store can be locked, it must be unlocked first before inserting currency. Stops traitor checking.
-            if (!uplink.Unlocked)
-                args.Cancel();
-        }
-
-        private void RingerPlayRingtone(EntityUid uid, RingerComponent ringer, RingerPlayRingtoneMessage args)
-        {
-            EnsureComp<ActiveRingerComponent>(uid);
-
-            _popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), uid, Filter.Pvs(uid, 0.05f), false, PopupType.Small);
-
-            UpdateRingerUserInterface(uid, ringer, true);
-        }
-
-        public void RingerPlayRingtone(Entity<RingerComponent?> ent)
-        {
-            if (!Resolve(ent, ref ent.Comp))
-                return;
+namespace Content.Server.PDA.Ringer;
 
-            EnsureComp<ActiveRingerComponent>(ent);
-
-            _popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), ent, Filter.Pvs(ent, 0.05f), false, PopupType.Medium);
-
-            UpdateRingerUserInterface(ent, ent.Comp, true);
-        }
-
-        private void UpdateRingerUserInterfaceDriver(EntityUid uid, RingerComponent ringer, RingerRequestUpdateInterfaceMessage args)
-        {
-            UpdateRingerUserInterface(uid, ringer, HasComp<ActiveRingerComponent>(uid));
-        }
-
-        private void OnSetRingtone(EntityUid uid, RingerComponent ringer, RingerSetRingtoneMessage args)
-        {
-            if (!TryComp(args.Actor, out ActorComponent? actorComp))
-                return;
-
-            ref var lastSetAt = ref CollectionsMarshal.GetValueRefOrAddDefault(_lastSetRingtoneAt, actorComp.PlayerSession.UserId, out var exists);
-
-            // Delay on the client is 0.333, 0.25 is still enough and gives some leeway in case of small time differences
-            if (exists && lastSetAt > _gameTiming.CurTime - TimeSpan.FromMilliseconds(250))
-                return;
-
-            lastSetAt = _gameTiming.CurTime;
-
-            // Client sent us an updated ringtone so set it to that.
-            if (args.Ringtone.Length != RingtoneLength)
-                return;
+/// <summary>
+/// Handles the server-side logic for <see cref="SharedRingerSystem"/>.
+/// </summary>
+public sealed class RingerSystem : SharedRingerSystem
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
 
-            var ev = new BeforeRingtoneSetEvent(args.Ringtone);
-            RaiseLocalEvent(uid, ref ev);
-            if (ev.Handled)
-                return;
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
 
-            UpdateRingerRingtone(uid, ringer, args.Ringtone);
-        }
+        SubscribeLocalEvent<RingerComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
 
-        private void OnSetUplinkRingtone(EntityUid uid, RingerUplinkComponent uplink, ref BeforeRingtoneSetEvent args)
-        {
-            if (uplink.Code.SequenceEqual(args.Ringtone) && HasComp<StoreComponent>(uid))
-            {
-                uplink.Unlocked = !uplink.Unlocked;
-                if (TryComp<PdaComponent>(uid, out var pda))
-                    _pda.UpdatePdaUi(uid, pda);
-
-                // can't keep store open after locking it
-                if (!uplink.Unlocked)
-                    _ui.CloseUi(uid, StoreUiKey.Key);
-
-                // no saving the code to prevent meta click set on sus guys pda -> wewlad
-                args.Handled = true;
-            }
-        }
-
-        /// <summary>
-        /// Locks the uplink and closes the window, if its open
-        /// </summary>
-        /// <remarks>
-        /// Will not update the PDA ui so you must do that yourself if needed
-        /// </remarks>
-        public void LockUplink(EntityUid uid, RingerUplinkComponent? uplink)
-        {
-            if (!Resolve(uid, ref uplink, true))
-                return;
+        SubscribeLocalEvent<RingerUplinkComponent, GenerateUplinkCodeEvent>(OnGenerateUplinkCode);
+    }
 
-            uplink.Unlocked = false;
-            _ui.CloseUi(uid, StoreUiKey.Key);
-        }
+    /// <summary>
+    /// Randomizes a ringtone for <see cref="RingerComponent"/> on <see cref="MapInitEvent"/>.
+    /// </summary>
+    private void OnMapInit(Entity<RingerComponent> ent, ref MapInitEvent args)
+    {
+        UpdateRingerRingtone(ent, GenerateRingtone());
+    }
 
-        public void RandomizeRingtone(EntityUid uid, RingerComponent ringer, MapInitEvent args)
+    /// <summary>
+    /// Handles the <see cref="CurrencyInsertAttemptEvent"/> for <see cref="RingerUplinkComponent"/>.
+    /// </summary>
+    private void OnCurrencyInsert(Entity<RingerComponent> ent, ref CurrencyInsertAttemptEvent args)
+    {
+        // TODO: Store isn't predicted, can't move it to shared
+        if (!TryComp<RingerUplinkComponent>(ent, out var uplink))
         {
-            UpdateRingerRingtone(uid, ringer, GenerateRingtone());
+            args.Cancel();
+            return;
         }
 
-        public void RandomizeUplinkCode(EntityUid uid, RingerUplinkComponent uplink, ComponentInit args)
-        {
-            uplink.Code = GenerateRingtone();
-        }
+        // if the store can be locked, it must be unlocked first before inserting currency. Stops traitor checking.
+        if (!uplink.Unlocked)
+            args.Cancel();
+    }
 
-        //Non Event Functions
+    /// <summary>
+    /// Handles the <see cref="GenerateUplinkCodeEvent"/> for generating an uplink code.
+    /// </summary>
+    private void OnGenerateUplinkCode(Entity<RingerUplinkComponent> ent, ref GenerateUplinkCodeEvent ev)
+    {
+        // Generate a new uplink code
+        var code = GenerateRingtone();
 
-        private Note[] GenerateRingtone()
-        {
-            // Default to using C pentatonic so it at least sounds not terrible.
-            return GenerateRingtone(new[]
-            {
-                Note.C,
-                Note.D,
-                Note.E,
-                Note.G,
-                Note.A
-            });
-        }
+        // Set the code on the component
+        ent.Comp.Code = code;
 
-        private Note[] GenerateRingtone(Note[] notes)
-        {
-            var ringtone = new Note[RingtoneLength];
+        // Return the code via the event
+        ev.Code = code;
+    }
 
-            for (var i = 0; i < RingtoneLength; i++)
-            {
-                ringtone[i] = _random.Pick(notes);
-            }
+    /// <inheritdoc/>
+    public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null)
+    {
+        if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
+            return false;
 
-            return ringtone;
-        }
+        if (!HasComp<StoreComponent>(uid))
+            return false;
 
-        private bool UpdateRingerRingtone(EntityUid uid, RingerComponent ringer, Note[] ringtone)
-        {
-            // Assume validation has already happened.
-            ringer.Ringtone = ringtone;
-            UpdateRingerUserInterface(uid, ringer, HasComp<ActiveRingerComponent>(uid));
+        // On the server, we always check if the code matches
+        if (!uplink.Code.SequenceEqual(ringtone))
+            return false;
 
-            return true;
-        }
+        return ToggleUplinkInternal((uid, uplink));
+    }
 
-        private void UpdateRingerUserInterface(EntityUid uid, RingerComponent ringer, bool isPlaying)
-        {
-            _ui.SetUiState(uid, RingerUiKey.Key, new RingerUpdateState(isPlaying, ringer.Ringtone));
-        }
+    /// <summary>
+    /// Generates a random ringtone using the C pentatonic scale.
+    /// </summary>
+    /// <returns>An array of Notes representing the ringtone.</returns>
+    /// <remarks>The logic for this is on the Server so that we don't get a different result on the Client every time.</remarks>
+    private Note[] GenerateRingtone()
+    {
+        // Default to using C pentatonic so it at least sounds not terrible.
+        return GenerateRingtone(new[]
+        {
+            Note.C,
+            Note.D,
+            Note.E,
+            Note.G,
+            Note.A
+        });
+    }
 
-        public bool ToggleRingerUI(EntityUid uid, EntityUid actor)
-        {
-            _ui.TryToggleUi(uid, RingerUiKey.Key, actor);
-            return true;
-        }
+    /// <summary>
+    /// Generates a random ringtone using the specified notes.
+    /// </summary>
+    /// <param name="notes">The notes to choose from when generating the ringtone.</param>
+    /// <returns>An array of Notes representing the ringtone.</returns>
+    /// <remarks>The logic for this is on the Server so that we don't get a different result on the Client every time.</remarks>
+    private Note[] GenerateRingtone(Note[] notes)
+    {
+        var ringtone = new Note[RingtoneLength];
 
-        public override void Update(float frameTime) //Responsible for actually playing the ringtone
+        for (var i = 0; i < RingtoneLength; i++)
         {
-            var remove = new RemQueue<EntityUid>();
-
-            var pdaQuery = EntityQueryEnumerator<RingerComponent, ActiveRingerComponent>();
-            while (pdaQuery.MoveNext(out var uid, out var ringer, out var _))
-            {
-                ringer.TimeElapsed += frameTime;
-
-                if (ringer.TimeElapsed < NoteDelay)
-                    continue;
-
-                ringer.TimeElapsed -= NoteDelay;
-                var ringerXform = Transform(uid);
-
-                _audio.PlayEntity(
-                    GetSound(ringer.Ringtone[ringer.NoteCount]),
-                    Filter.Empty().AddInRange(_transform.GetMapCoordinates(uid, ringerXform), ringer.Range),
-                    uid,
-                    true,
-                    AudioParams.Default.WithMaxDistance(ringer.Range).WithVolume(ringer.Volume)
-                );
-
-                ringer.NoteCount++;
-
-                if (ringer.NoteCount > RingtoneLength - 1)
-                {
-                    remove.Add(uid);
-                    UpdateRingerUserInterface(uid, ringer, false);
-                    ringer.TimeElapsed = 0;
-                    ringer.NoteCount = 0;
-                    break;
-                }
-            }
-
-            foreach (var ent in remove)
-            {
-                RemComp<ActiveRingerComponent>(ent);
-            }
+            ringtone[i] = _random.Pick(notes);
         }
 
-        private static string GetSound(Note note)
-        {
-            return new ResPath("/Audio/Effects/RingtoneNotes/" + note.ToString().ToLower()) + ".ogg";
-        }
+        return ringtone;
     }
+}
 
-    [ByRefEvent]
-    public record struct BeforeRingtoneSetEvent(Note[] Ringtone, bool Handled = false);
+/// <summary>
+/// Event raised to generate a new uplink code for a PDA.
+/// </summary>
+[ByRefEvent]
+public record struct GenerateUplinkCodeEvent
+{
+    /// <summary>
+    /// The generated uplink code (filled in by the event handler).
+    /// </summary>
+    public Note[]? Code;
 }
diff --git a/Content.Server/PDA/Ringer/RingerUplinkComponent.cs b/Content.Server/PDA/Ringer/RingerUplinkComponent.cs
deleted file mode 100644 (file)
index 4ffedf3..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-using Content.Shared.PDA;
-
-namespace Content.Server.PDA.Ringer;
-
-/// <summary>
-/// Opens the store ui when the ringstone is set to the secret code.
-/// Traitors are told the code when greeted.
-/// </summary>
-[RegisterComponent, Access(typeof(RingerSystem))]
-public sealed partial class RingerUplinkComponent : Component
-{
-    /// <summary>
-    /// Notes to set ringtone to in order to lock or unlock the uplink.
-    /// Automatically initialized to random notes.
-    /// </summary>
-    [DataField("code")]
-    public Note[] Code = new Note[RingerSystem.RingtoneLength];
-
-    /// <summary>
-    /// Whether to show the toggle uplink button in pda settings.
-    /// </summary>
-    [DataField("unlocked"), ViewVariables(VVAccess.ReadWrite)]
-    public bool Unlocked;
-}
index 93bbd3bd966737bf0b11343f59b91082d570a50e..587270bd641160b8eb099af9003da7c9437d02f1 100644 (file)
@@ -1,7 +1,6 @@
 using System.Linq;
 using Content.Server.Actions;
 using Content.Server.Administration.Logs;
-using Content.Server.PDA.Ringer;
 using Content.Server.Stack;
 using Content.Server.Store.Components;
 using Content.Shared.Actions;
@@ -9,6 +8,7 @@ using Content.Shared.Database;
 using Content.Shared.FixedPoint;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Mind;
+using Content.Shared.PDA.Ringer;
 using Content.Shared.Store;
 using Content.Shared.Store.Components;
 using Content.Shared.UserInterface;
diff --git a/Content.Shared/PDA/Ringer/RingerComponent.cs b/Content.Shared/PDA/Ringer/RingerComponent.cs
new file mode 100644 (file)
index 0000000..81cdb25
--- /dev/null
@@ -0,0 +1,57 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.PDA.Ringer;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRingerSystem))]
+[AutoGenerateComponentState(true, fieldDeltas: true), AutoGenerateComponentPause]
+public sealed partial class RingerComponent : Component
+{
+    /// <summary>
+    /// The ringtone, represented as an array of notes.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Note[] Ringtone = new Note[SharedRingerSystem.RingtoneLength];
+
+    /// <summary>
+    /// The last time this ringer's ringtone was set.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
+    public TimeSpan NextRingtoneSetTime;
+
+    /// <summary>
+    /// The time when the next note should play.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
+    public TimeSpan? NextNoteTime;
+
+    /// <summary>
+    /// The cooldown before the ringtone can be changed again.
+    /// </summary>
+    [DataField]
+    public TimeSpan Cooldown = TimeSpan.FromMilliseconds(250);
+
+    /// <summary>
+    /// Keeps track of how many notes have elapsed if the ringer component is playing.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public int NoteCount;
+
+    /// <summary>
+    /// How far the sound projects in metres.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float Range = 3f;
+
+    /// <summary>
+    /// The ringtone volume.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float Volume = -4f;
+
+    /// <summary>
+    /// Whether the ringer is currently playing its ringtone.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Active;
+}
index 36867284b4e2dbf37590e51a740368c1ae7ee423..d737c7b3b4862146e5825b1f6af0dbb7effcf9ed 100644 (file)
@@ -1,26 +1,17 @@
 using Robust.Shared.Serialization;
 
-namespace Content.Shared.PDA.Ringer
-{
+namespace Content.Shared.PDA.Ringer;
 
-    [Serializable, NetSerializable]
-    public sealed class RingerRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
-    {
-    }
+[Serializable, NetSerializable]
+public sealed class RingerPlayRingtoneMessage : BoundUserInterfaceMessage;
 
-    [Serializable, NetSerializable]
-    public sealed class RingerPlayRingtoneMessage : BoundUserInterfaceMessage
-    {
-    }
+[Serializable, NetSerializable]
+public sealed class RingerSetRingtoneMessage : BoundUserInterfaceMessage
+{
+    public Note[] Ringtone { get; }
 
-    [Serializable, NetSerializable]
-    public sealed class RingerSetRingtoneMessage : BoundUserInterfaceMessage
+    public RingerSetRingtoneMessage(Note[] ringTone)
     {
-        public Note[] Ringtone { get; }
-
-        public RingerSetRingtoneMessage(Note[] ringTone)
-        {
-            Ringtone = ringTone;
-        }
+        Ringtone = ringTone;
     }
 }
diff --git a/Content.Shared/PDA/Ringer/RingerUpdateState.cs b/Content.Shared/PDA/Ringer/RingerUpdateState.cs
deleted file mode 100644 (file)
index 9d2eccd..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.PDA.Ringer
-{
-    [Serializable, NetSerializable]
-    public sealed class RingerUpdateState : BoundUserInterfaceState
-    {
-        public bool IsPlaying;
-        public Note[] Ringtone;
-
-        public RingerUpdateState(bool isPlay, Note[] ringtone)
-        {
-            IsPlaying = isPlay;
-            Ringtone = ringtone;
-        }
-    }
-
-}
diff --git a/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs b/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs
new file mode 100644 (file)
index 0000000..e3170c8
--- /dev/null
@@ -0,0 +1,24 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.PDA.Ringer;
+
+/// <summary>
+/// Opens the store UI when the ringstone is set to the secret code.
+/// Traitors are told the code when greeted.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedRingerSystem))]
+public sealed partial class RingerUplinkComponent : Component
+{
+    /// <summary>
+    /// Notes to set ringtone to in order to lock or unlock the uplink.
+    /// Set via GenerateUplinkCodeEvent.
+    /// </summary>
+    [DataField]
+    public Note[] Code = new Note[SharedRingerSystem.RingtoneLength];
+
+    /// <summary>
+    /// Whether to show the toggle uplink button in PDA settings.
+    /// </summary>
+    [DataField]
+    public bool Unlocked;
+}
index 44e5e818d3000f97670f7311ab6473a4eaeaf89b..32c9fb3a38444d8375d77a2801032584383cb13b 100644 (file)
@@ -1,11 +1,9 @@
 using Robust.Shared.Serialization;
 
-namespace Content.Shared.PDA.Ringer
-{
-    [Serializable, NetSerializable]
-    public enum RingerUiKey
-    {
-        Key
-    }
+namespace Content.Shared.PDA.Ringer;
 
+[Serializable, NetSerializable]
+public enum RingerUiKey : byte
+{
+    Key,
 }
index bd291a3843eb49b7d23c6d9512efc4287fa3525f..38a3899f7fdbabba97c0beb9a337a8edc7e70cc5 100644 (file)
@@ -66,5 +66,11 @@ namespace Content.Shared.PDA
         {
             Appearance.SetData(uid, PdaVisuals.IdCardInserted, pda.ContainedId != null);
         }
+
+        public virtual void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null)
+        {
+            // This does nothing yet while I finish up PDA prediction
+            // Overriden by the server
+        }
     }
 }
index 5652a60599832a54fd2ddd115356cc747696033c..278ab94307519a53b61ded79dddf867742b00b90 100644 (file)
+using Content.Shared.Mind;
+using Content.Shared.PDA.Ringer;
+using Content.Shared.Popups;
+using Content.Shared.Roles;
+using Content.Shared.Store;
+using JetBrains.Annotations;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
 using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.PDA;
 
+/// <summary>
+/// Handles the shared functionality for PDA ringtones.
+/// </summary>
 public abstract class SharedRingerSystem : EntitySystem
 {
     public const int RingtoneLength = 6;
     public const int NoteTempo = 300;
     public const float NoteDelay = 60f / NoteTempo;
+
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly SharedPdaSystem _pda = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedRoleSystem _role = default!;
+    [Dependency] private readonly SharedTransformSystem _xform = default!;
+    [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        // RingerBoundUserInterface Subscriptions
+        SubscribeLocalEvent<RingerComponent, RingerSetRingtoneMessage>(OnSetRingtone);
+        SubscribeLocalEvent<RingerComponent, RingerPlayRingtoneMessage>(OnPlayRingtone);
+    }
+
+    /// <inheritdoc/>
+    public override void Update(float frameTime)
+    {
+        var ringerQuery = EntityQueryEnumerator<RingerComponent>();
+        while (ringerQuery.MoveNext(out var uid, out var ringer))
+        {
+            if (!ringer.Active || !ringer.NextNoteTime.HasValue)
+                continue;
+
+            var curTime = _timing.CurTime;
+
+            // Check if it's time to play the next note
+            if (curTime < ringer.NextNoteTime.Value)
+                continue;
+
+            // Play the note
+            // We only do this on the server because otherwise the sound either dupes or blends into a mess
+            // There's no easy way to figure out which player started it, so that we can exclude them from the list
+            // and play it separately with PlayLocal, so that it's actually predicted
+            if (_net.IsServer)
+            {
+                var ringerXform = Transform(uid);
+                _audio.PlayEntity(
+                    GetSound(ringer.Ringtone[ringer.NoteCount]),
+                    Filter.Empty().AddInRange(_xform.GetMapCoordinates(uid, ringerXform), ringer.Range),
+                    uid,
+                    true,
+                    AudioParams.Default.WithMaxDistance(ringer.Range).WithVolume(ringer.Volume)
+                );
+            }
+
+            // Schedule next note
+            ringer.NextNoteTime = curTime + TimeSpan.FromSeconds(NoteDelay);
+            ringer.NoteCount++;
+
+            // Dirty the fields we just changed
+            DirtyFields(uid,
+                ringer,
+                null,
+                nameof(RingerComponent.NextNoteTime),
+                nameof(RingerComponent.NoteCount));
+
+            // Check if we've finished playing all notes
+            if (ringer.NoteCount >= RingtoneLength)
+            {
+                ringer.Active = false;
+                ringer.NextNoteTime = null;
+                ringer.NoteCount = 0;
+
+                DirtyFields(uid,
+                    ringer,
+                    null,
+                    nameof(RingerComponent.Active),
+                    nameof(RingerComponent.NextNoteTime),
+                    nameof(RingerComponent.NoteCount));
+
+                UpdateRingerUi((uid, ringer));
+            }
+        }
+    }
+
+    #region Public API
+
+    /// <summary>
+    /// Plays the ringtone on the device with the given RingerComponent.
+    /// </summary>
+    public void RingerPlayRingtone(Entity<RingerComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        StartRingtone((ent, ent.Comp));
+    }
+
+    /// <summary>
+    /// Toggles the ringer UI for the given entity.
+    /// </summary>
+    /// <param name="uid">The entity containing the ringer UI.</param>
+    /// <param name="actor">The entity that's interacting with the UI.</param>
+    /// <returns>True if the UI toggle was successful.</returns>
+    public bool TryToggleRingerUi(EntityUid uid, EntityUid actor)
+    {
+        UI.TryToggleUi(uid, RingerUiKey.Key, actor);
+        return true;
+    }
+
+    /// <summary>
+    /// Locks the uplink and closes the window, if its open.
+    /// </summary>
+    /// <remarks>
+    /// Will not update the PDA ui so you must do that yourself if needed.
+    /// </remarks>
+    public void LockUplink(Entity<RingerUplinkComponent?> ent)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        ent.Comp.Unlocked = false;
+        UI.CloseUi(ent.Owner, StoreUiKey.Key);
+    }
+
+    /// <summary>
+    /// Attempts to unlock or lock the uplink by checking the provided ringtone against the uplink code.
+    /// On the client side, for antagonists, the code check is skipped to support prediction.
+    /// On the server side, the code is always verified.
+    /// </summary>
+    /// <param name="uid">The entity with the RingerUplinkComponent.</param>
+    /// <param name="ringtone">The ringtone to check against the uplink code.</param>
+    /// <param name="user">The entity attempting to toggle the uplink. If the user is an antagonist,
+    /// the ringtone code check will be skipped on the client to allow prediction.</param>
+    /// <returns>True if the uplink state was toggled, false otherwise.</returns>
+    [PublicAPI]
+    public abstract bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null);
+
+    #endregion
+
+    // UI Message event handlers
+
+    /// <summary>
+    /// Handles the <see cref="RingerSetRingtoneMessage"/> from the client UI.
+    /// </summary>
+    private void OnSetRingtone(Entity<RingerComponent> ent, ref RingerSetRingtoneMessage args)
+    {
+        // Prevent ringtone spam by checking the last time this ringtone was set
+        var curTime = _timing.CurTime;
+        if (ent.Comp.NextRingtoneSetTime > curTime)
+            return;
+
+        ent.Comp.NextRingtoneSetTime = curTime + ent.Comp.Cooldown;
+        DirtyField(ent.AsNullable(), nameof(RingerComponent.NextRingtoneSetTime));
+
+        // Client sent us an updated ringtone so set it to that.
+        if (args.Ringtone.Length != RingtoneLength)
+            return;
+
+        // Try to toggle the uplink first
+        if (TryToggleUplink(ent, args.Ringtone))
+            return; // Don't save the uplink code as the ringtone
+
+        UpdateRingerRingtone(ent, args.Ringtone);
+    }
+
+    /// <summary>
+    /// Handles the <see cref="RingerPlayRingtoneMessage"/> from the client UI.
+    /// </summary>
+    private void OnPlayRingtone(Entity<RingerComponent> ent, ref RingerPlayRingtoneMessage args)
+    {
+        StartRingtone(ent);
+    }
+
+    // Helper methods
+
+    /// <summary>
+    /// Starts playing the ringtone on the device.
+    /// </summary>
+    private void StartRingtone(Entity<RingerComponent> ent)
+    {
+        // Already active? Don't start it again
+        if (ent.Comp.Active)
+            return;
+
+        ent.Comp.Active = true;
+        ent.Comp.NoteCount = 0;
+        ent.Comp.NextNoteTime = _timing.CurTime;
+
+        UpdateRingerUi(ent);
+
+        _popup.PopupPredicted(Loc.GetString("comp-ringer-vibration-popup"),
+            ent,
+            ent.Owner,
+            Filter.Pvs(ent, 0.05f),
+            false,
+            PopupType.Medium);
+
+        DirtyFields(ent.AsNullable(),
+            null,
+            nameof(RingerComponent.NextNoteTime),
+            nameof(RingerComponent.Active),
+            nameof(RingerComponent.NoteCount));
+    }
+
+    /// <summary>
+    /// Updates the ringer's ringtone and notifies clients.
+    /// </summary>
+    /// <param name="ent">Entity with RingerComponent to update.</param>
+    /// <param name="ringtone">The new ringtone to set.</param>
+    protected void UpdateRingerRingtone(Entity<RingerComponent> ent, Note[] ringtone)
+    {
+        // Assume validation has already happened.
+        ent.Comp.Ringtone = ringtone;
+        DirtyField(ent.AsNullable(), nameof(RingerComponent.Ringtone));
+        UpdateRingerUi(ent);
+    }
+
+    /// <summary>
+    /// Base implementation for toggle uplink processing after verification.
+    /// </summary>
+    protected bool ToggleUplinkInternal(Entity<RingerUplinkComponent> ent)
+    {
+        // Toggle the unlock state
+        ent.Comp.Unlocked = !ent.Comp.Unlocked;
+
+        // Update PDA UI if needed
+        if (TryComp<PdaComponent>(ent, out var pda))
+            _pda.UpdatePdaUi(ent, pda);
+
+        // Close store UI if we're locking
+        if (!ent.Comp.Unlocked)
+            UI.CloseUi(ent.Owner, StoreUiKey.Key);
+
+        return true;
+    }
+
+    /// <summary>
+    /// Helper method to determine if the mind is an antagonist.
+    /// </summary>
+    protected bool IsAntagonist(EntityUid? user)
+    {
+        return user != null && _mind.TryGetMind(user.Value, out var mindId, out _) && _role.MindIsAntagonist(mindId);
+    }
+
+    /// <summary>
+    /// Gets the sound path for a specific note.
+    /// </summary>
+    /// <param name="note">The note to get the sound for.</param>
+    /// <returns>A SoundPathSpecifier pointing to the sound file for the note.</returns>
+    private static SoundPathSpecifier GetSound(Note note)
+    {
+        return new SoundPathSpecifier($"/Audio/Effects/RingtoneNotes/{note.ToString().ToLower()}.ogg");
+    }
+
+    /// <summary>
+    /// Updates the RingerBoundUserInterface.
+    /// </summary>
+    protected virtual void UpdateRingerUi(Entity<RingerComponent> ent)
+    {
+    }
 }
 
+/// <summary>
+/// Enum representing musical notes for ringtones.
+/// </summary>
 [Serializable, NetSerializable]
 public enum Note : byte
 {
index 1f52c154c7fce5dbb3e866dbc29046cc9004b9a1..e47d6d2ec7833ed1b25751b456cfa38cec7c9ad6 100644 (file)
@@ -68,6 +68,7 @@
     softness: 5
     autoRot: true
   - type: Ringer
+  - type: RingerUplink
   - type: DeviceNetwork
     deviceNetId: Wireless
     receiveFrequencyId: PDA