From: Milon Date: Sat, 19 Apr 2025 13:55:05 +0000 (+0200) Subject: Predict and cleanup RingerComponent (#35907) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=6138fcdce9f306c15fd86eaa1884e79e85a9382a;p=space-station-14.git Predict and cleanup RingerComponent (#35907) * clean up most stuff * move to shared * works * shuffle shit around * oops! access * fixes * todo: everything * SUFFERING * curse you --- diff --git a/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs b/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs index 170a296ac2..fc778de7d9 100644 --- a/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs +++ b/Content.Client/PDA/Ringer/RingerBoundUserInterface.cs @@ -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(); _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 index 0000000000..6d45a8ce8b --- /dev/null +++ b/Content.Client/PDA/Ringer/RingerSystem.cs @@ -0,0 +1,56 @@ +using Content.Shared.PDA; +using Content.Shared.PDA.Ringer; +using Content.Shared.Store.Components; + +namespace Content.Client.PDA.Ringer; + +/// +/// Handles the client-side logic for . +/// +public sealed class RingerSystem : SharedRingerSystem +{ + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRingerUpdate); + } + + /// + /// Updates the UI whenever we get a new component state from the server. + /// + private void OnRingerUpdate(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateRingerUi(ent); + } + + /// + protected override void UpdateRingerUi(Entity ent) + { + if (UI.TryGetOpenUi(ent.Owner, RingerUiKey.Key, out var bui)) + { + bui.Update(); + } + } + + /// + public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null) + { + if (!TryComp(uid, out var uplink)) + return false; + + if (!HasComp(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; + } +} diff --git a/Content.Client/PDA/Ringer/RingtoneMenu.xaml b/Content.Client/PDA/Ringer/RingtoneMenu.xaml index 2fff0ab1b5..1bef4d433f 100644 --- a/Content.Client/PDA/Ringer/RingtoneMenu.xaml +++ b/Content.Client/PDA/Ringer/RingtoneMenu.xaml @@ -1,7 +1,8 @@ - + - + diff --git a/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs b/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs index 044714b06f..989fe65843 100644 --- a/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs +++ b/Content.Client/PDA/Ringer/RingtoneMenu.xaml.cs @@ -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; }; } } diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 4d041af3a0..790b14579e 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -184,13 +184,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem { Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA"); // Codes are only generated if the uplink is a PDA - code = EnsureComp(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) { diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs index bdf688efe7..bfa8e1825d 100644 --- a/Content.Server/PDA/PdaSystem.cs +++ b/Content.Server/PDA/PdaSystem.cs @@ -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 /// /// Send new UI state to clients, call if you modify something like uplink. /// - 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(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(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 index 55dc458b74..0000000000 --- a/Content.Server/PDA/Ringer/RingerComponent.cs +++ /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; - - /// - /// Keeps track of how many notes have elapsed if the ringer component is playing. - /// - [DataField("noteCount")] - public int NoteCount = 0; - - /// - /// How far the sound projects in metres. - /// - [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 - { - } -} diff --git a/Content.Server/PDA/Ringer/RingerSystem.cs b/Content.Server/PDA/Ringer/RingerSystem.cs index 0cc4ea86c2..dbdc5e83f3 100644 --- a/Content.Server/PDA/Ringer/RingerSystem.cs +++ b/Content.Server/PDA/Ringer/RingerSystem.cs @@ -1,253 +1,131 @@ 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 _lastSetRingtoneAt = new(); - - public override void Initialize() - { - base.Initialize(); - - // General Event Subscriptions - SubscribeLocalEvent(RandomizeRingtone); - SubscribeLocalEvent(RandomizeUplinkCode); - // RingerBoundUserInterface Subscriptions - SubscribeLocalEvent(OnSetRingtone); - SubscribeLocalEvent(OnSetUplinkRingtone); - SubscribeLocalEvent(RingerPlayRingtone); - SubscribeLocalEvent(UpdateRingerUserInterfaceDriver); - - SubscribeLocalEvent(OnCurrencyInsert); - } - - //Event Functions - - private void OnCurrencyInsert(EntityUid uid, RingerComponent ringer, CurrencyInsertAttemptEvent args) - { - if (!TryComp(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(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 ent) - { - if (!Resolve(ent, ref ent.Comp)) - return; +namespace Content.Server.PDA.Ringer; - EnsureComp(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(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; +/// +/// Handles the server-side logic for . +/// +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; + /// + public override void Initialize() + { + base.Initialize(); - UpdateRingerRingtone(uid, ringer, args.Ringtone); - } + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnCurrencyInsert); - private void OnSetUplinkRingtone(EntityUid uid, RingerUplinkComponent uplink, ref BeforeRingtoneSetEvent args) - { - if (uplink.Code.SequenceEqual(args.Ringtone) && HasComp(uid)) - { - uplink.Unlocked = !uplink.Unlocked; - if (TryComp(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; - } - } - - /// - /// Locks the uplink and closes the window, if its open - /// - /// - /// Will not update the PDA ui so you must do that yourself if needed - /// - public void LockUplink(EntityUid uid, RingerUplinkComponent? uplink) - { - if (!Resolve(uid, ref uplink, true)) - return; + SubscribeLocalEvent(OnGenerateUplinkCode); + } - uplink.Unlocked = false; - _ui.CloseUi(uid, StoreUiKey.Key); - } + /// + /// Randomizes a ringtone for on . + /// + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + UpdateRingerRingtone(ent, GenerateRingtone()); + } - public void RandomizeRingtone(EntityUid uid, RingerComponent ringer, MapInitEvent args) + /// + /// Handles the for . + /// + private void OnCurrencyInsert(Entity ent, ref CurrencyInsertAttemptEvent args) + { + // TODO: Store isn't predicted, can't move it to shared + if (!TryComp(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 + /// + /// Handles the for generating an uplink code. + /// + private void OnGenerateUplinkCode(Entity 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); - } + /// + public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null) + { + if (!TryComp(uid, out var uplink)) + return false; - return ringtone; - } + if (!HasComp(uid)) + return false; - private bool UpdateRingerRingtone(EntityUid uid, RingerComponent ringer, Note[] ringtone) - { - // Assume validation has already happened. - ringer.Ringtone = ringtone; - UpdateRingerUserInterface(uid, ringer, HasComp(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)); - } + /// + /// Generates a random ringtone using the C pentatonic scale. + /// + /// An array of Notes representing the ringtone. + /// The logic for this is on the Server so that we don't get a different result on the Client every time. + 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; - } + /// + /// Generates a random ringtone using the specified notes. + /// + /// The notes to choose from when generating the ringtone. + /// An array of Notes representing the ringtone. + /// The logic for this is on the Server so that we don't get a different result on the Client every time. + 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(); - - var pdaQuery = EntityQueryEnumerator(); - 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(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); +/// +/// Event raised to generate a new uplink code for a PDA. +/// +[ByRefEvent] +public record struct GenerateUplinkCodeEvent +{ + /// + /// The generated uplink code (filled in by the event handler). + /// + public Note[]? Code; } diff --git a/Content.Server/PDA/Ringer/RingerUplinkComponent.cs b/Content.Server/PDA/Ringer/RingerUplinkComponent.cs deleted file mode 100644 index 4ffedf3af4..0000000000 --- a/Content.Server/PDA/Ringer/RingerUplinkComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.PDA; - -namespace Content.Server.PDA.Ringer; - -/// -/// Opens the store ui when the ringstone is set to the secret code. -/// Traitors are told the code when greeted. -/// -[RegisterComponent, Access(typeof(RingerSystem))] -public sealed partial class RingerUplinkComponent : Component -{ - /// - /// Notes to set ringtone to in order to lock or unlock the uplink. - /// Automatically initialized to random notes. - /// - [DataField("code")] - public Note[] Code = new Note[RingerSystem.RingtoneLength]; - - /// - /// Whether to show the toggle uplink button in pda settings. - /// - [DataField("unlocked"), ViewVariables(VVAccess.ReadWrite)] - public bool Unlocked; -} diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 93bbd3bd96..587270bd64 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -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 index 0000000000..81cdb2522c --- /dev/null +++ b/Content.Shared/PDA/Ringer/RingerComponent.cs @@ -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 +{ + /// + /// The ringtone, represented as an array of notes. + /// + [DataField, AutoNetworkedField] + public Note[] Ringtone = new Note[SharedRingerSystem.RingtoneLength]; + + /// + /// The last time this ringer's ringtone was set. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan NextRingtoneSetTime; + + /// + /// The time when the next note should play. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan? NextNoteTime; + + /// + /// The cooldown before the ringtone can be changed again. + /// + [DataField] + public TimeSpan Cooldown = TimeSpan.FromMilliseconds(250); + + /// + /// Keeps track of how many notes have elapsed if the ringer component is playing. + /// + [DataField, AutoNetworkedField] + public int NoteCount; + + /// + /// How far the sound projects in metres. + /// + [DataField, AutoNetworkedField] + public float Range = 3f; + + /// + /// The ringtone volume. + /// + [DataField, AutoNetworkedField] + public float Volume = -4f; + + /// + /// Whether the ringer is currently playing its ringtone. + /// + [DataField, AutoNetworkedField] + public bool Active; +} diff --git a/Content.Shared/PDA/Ringer/RingerMessagesUI.cs b/Content.Shared/PDA/Ringer/RingerMessagesUI.cs index 36867284b4..d737c7b3b4 100644 --- a/Content.Shared/PDA/Ringer/RingerMessagesUI.cs +++ b/Content.Shared/PDA/Ringer/RingerMessagesUI.cs @@ -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 index 9d2eccd1a7..0000000000 --- a/Content.Shared/PDA/Ringer/RingerUpdateState.cs +++ /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 index 0000000000..e3170c84e3 --- /dev/null +++ b/Content.Shared/PDA/Ringer/RingerUplinkComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.PDA.Ringer; + +/// +/// Opens the store UI when the ringstone is set to the secret code. +/// Traitors are told the code when greeted. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedRingerSystem))] +public sealed partial class RingerUplinkComponent : Component +{ + /// + /// Notes to set ringtone to in order to lock or unlock the uplink. + /// Set via GenerateUplinkCodeEvent. + /// + [DataField] + public Note[] Code = new Note[SharedRingerSystem.RingtoneLength]; + + /// + /// Whether to show the toggle uplink button in PDA settings. + /// + [DataField] + public bool Unlocked; +} diff --git a/Content.Shared/PDA/Ringer/RingerVisuals.cs b/Content.Shared/PDA/Ringer/RingerVisuals.cs index 44e5e818d3..32c9fb3a38 100644 --- a/Content.Shared/PDA/Ringer/RingerVisuals.cs +++ b/Content.Shared/PDA/Ringer/RingerVisuals.cs @@ -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, } diff --git a/Content.Shared/PDA/SharedPdaSystem.cs b/Content.Shared/PDA/SharedPdaSystem.cs index bd291a3843..38a3899f7f 100644 --- a/Content.Shared/PDA/SharedPdaSystem.cs +++ b/Content.Shared/PDA/SharedPdaSystem.cs @@ -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 + } } } diff --git a/Content.Shared/PDA/SharedRingerSystem.cs b/Content.Shared/PDA/SharedRingerSystem.cs index 5652a60599..278ab94307 100644 --- a/Content.Shared/PDA/SharedRingerSystem.cs +++ b/Content.Shared/PDA/SharedRingerSystem.cs @@ -1,14 +1,289 @@ +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; +/// +/// Handles the shared functionality for PDA ringtones. +/// 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!; + + /// + public override void Initialize() + { + base.Initialize(); + + // RingerBoundUserInterface Subscriptions + SubscribeLocalEvent(OnSetRingtone); + SubscribeLocalEvent(OnPlayRingtone); + } + + /// + public override void Update(float frameTime) + { + var ringerQuery = EntityQueryEnumerator(); + 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 + + /// + /// Plays the ringtone on the device with the given RingerComponent. + /// + public void RingerPlayRingtone(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + StartRingtone((ent, ent.Comp)); + } + + /// + /// Toggles the ringer UI for the given entity. + /// + /// The entity containing the ringer UI. + /// The entity that's interacting with the UI. + /// True if the UI toggle was successful. + public bool TryToggleRingerUi(EntityUid uid, EntityUid actor) + { + UI.TryToggleUi(uid, RingerUiKey.Key, actor); + return true; + } + + /// + /// Locks the uplink and closes the window, if its open. + /// + /// + /// Will not update the PDA ui so you must do that yourself if needed. + /// + public void LockUplink(Entity ent) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + ent.Comp.Unlocked = false; + UI.CloseUi(ent.Owner, StoreUiKey.Key); + } + + /// + /// 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. + /// + /// The entity with the RingerUplinkComponent. + /// The ringtone to check against the uplink code. + /// 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. + /// True if the uplink state was toggled, false otherwise. + [PublicAPI] + public abstract bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null); + + #endregion + + // UI Message event handlers + + /// + /// Handles the from the client UI. + /// + private void OnSetRingtone(Entity 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); + } + + /// + /// Handles the from the client UI. + /// + private void OnPlayRingtone(Entity ent, ref RingerPlayRingtoneMessage args) + { + StartRingtone(ent); + } + + // Helper methods + + /// + /// Starts playing the ringtone on the device. + /// + private void StartRingtone(Entity 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)); + } + + /// + /// Updates the ringer's ringtone and notifies clients. + /// + /// Entity with RingerComponent to update. + /// The new ringtone to set. + protected void UpdateRingerRingtone(Entity ent, Note[] ringtone) + { + // Assume validation has already happened. + ent.Comp.Ringtone = ringtone; + DirtyField(ent.AsNullable(), nameof(RingerComponent.Ringtone)); + UpdateRingerUi(ent); + } + + /// + /// Base implementation for toggle uplink processing after verification. + /// + protected bool ToggleUplinkInternal(Entity ent) + { + // Toggle the unlock state + ent.Comp.Unlocked = !ent.Comp.Unlocked; + + // Update PDA UI if needed + if (TryComp(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; + } + + /// + /// Helper method to determine if the mind is an antagonist. + /// + protected bool IsAntagonist(EntityUid? user) + { + return user != null && _mind.TryGetMind(user.Value, out var mindId, out _) && _role.MindIsAntagonist(mindId); + } + + /// + /// Gets the sound path for a specific note. + /// + /// The note to get the sound for. + /// A SoundPathSpecifier pointing to the sound file for the note. + private static SoundPathSpecifier GetSound(Note note) + { + return new SoundPathSpecifier($"/Audio/Effects/RingtoneNotes/{note.ToString().ToLower()}.ogg"); + } + + /// + /// Updates the RingerBoundUserInterface. + /// + protected virtual void UpdateRingerUi(Entity ent) + { + } } +/// +/// Enum representing musical notes for ringtones. +/// [Serializable, NetSerializable] public enum Note : byte { diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 1f52c154c7..e47d6d2ec7 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -68,6 +68,7 @@ softness: 5 autoRot: true - type: Ringer + - type: RingerUplink - type: DeviceNetwork deviceNetId: Wireless receiveFrequencyId: PDA