Predict and cleanup RingerComponent (#35907)
* clean up most stuff * move to shared * works * shuffle shit around * oops! access * fixes * todo: everything * SUFFERING * curse you
This commit is contained in:
@@ -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.TestRingtoneButtonPressed += OnTestRingtoneButtonPressed;
|
||||
_menu.SetRingtoneButtonPressed += OnSetRingtoneButtonPressed;
|
||||
|
||||
_menu.SetRingerButton.OnPressed += _ =>
|
||||
{
|
||||
if (!TryGetRingtone(out var ringtone))
|
||||
return;
|
||||
|
||||
SendMessage(new RingerSetRingtoneMessage(ringtone));
|
||||
_menu.SetRingerButton.Disabled = true;
|
||||
|
||||
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;
|
||||
|
||||
var note = msg.Ringtone[i].ToString();
|
||||
if (RingtoneMenu.IsNote(note))
|
||||
for (var i = 0; i < _menu.RingerNoteInputs.Length; i++)
|
||||
{
|
||||
var note = ringer.Ringtone[i].ToString();
|
||||
|
||||
if (!RingtoneMenu.IsNote(note))
|
||||
continue;
|
||||
|
||||
_menu.PreviousNoteInputs[i] = note.Replace("sharp", "#");
|
||||
_menu.RingerNoteInputs[i].Text = _menu.PreviousNoteInputs[i];
|
||||
}
|
||||
|
||||
_menu.TestRingerButton.Disabled = ringer.Active;
|
||||
}
|
||||
|
||||
_menu.TestRingerButton.Disabled = msg.IsPlaying;
|
||||
}
|
||||
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
private void OnTestRingtoneButtonPressed()
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
if (_menu is null)
|
||||
return;
|
||||
|
||||
_menu?.Dispose();
|
||||
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;
|
||||
}
|
||||
|
||||
private void OnSetRingtoneButtonPressed()
|
||||
{
|
||||
if (_menu is null)
|
||||
return;
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
Content.Client/PDA/Ringer/RingerSystem.cs
Normal file
56
Content.Client/PDA/Ringer/RingerSystem.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'comp-ringer-ui-menu-title'}"
|
||||
MinSize="320 128"
|
||||
SetSize="320 128">
|
||||
MinSize="320 100"
|
||||
SetSize="320 100">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
@@ -90,4 +91,4 @@
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -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.Text = input.Text.ToUpper();
|
||||
|
||||
if (!IsNote(input.Text))
|
||||
input.OnTextChanged += args =>
|
||||
{
|
||||
// Convert to uppercase
|
||||
var upperText = args.Text.ToUpper();
|
||||
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,12 @@ 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 (ev.Code is { } generatedCode)
|
||||
{
|
||||
code = generatedCode;
|
||||
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}",
|
||||
@@ -192,6 +197,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
return (code, briefing);
|
||||
}
|
||||
}
|
||||
else if (pda is null && uplinked)
|
||||
{
|
||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is implant");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,45 @@
|
||||
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
|
||||
namespace Content.Server.PDA.Ringer;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the server-side logic for <see cref="SharedRingerSystem"/>.
|
||||
/// </summary>
|
||||
public sealed class RingerSystem : SharedRingerSystem
|
||||
{
|
||||
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();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
|
||||
|
||||
SubscribeLocalEvent<RingerUplinkComponent, GenerateUplinkCodeEvent>(OnGenerateUplinkCode);
|
||||
}
|
||||
|
||||
//Event Functions
|
||||
|
||||
private void OnCurrencyInsert(EntityUid uid, RingerComponent ringer, CurrencyInsertAttemptEvent args)
|
||||
/// <summary>
|
||||
/// Randomizes a ringtone for <see cref="RingerComponent"/> on <see cref="MapInitEvent"/>.
|
||||
/// </summary>
|
||||
private void OnMapInit(Entity<RingerComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
|
||||
UpdateRingerRingtone(ent, GenerateRingtone());
|
||||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
args.Cancel();
|
||||
return;
|
||||
@@ -61,101 +50,42 @@ namespace Content.Server.PDA.Ringer
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void RingerPlayRingtone(EntityUid uid, RingerComponent ringer, RingerPlayRingtoneMessage args)
|
||||
/// <summary>
|
||||
/// Handles the <see cref="GenerateUplinkCodeEvent"/> for generating an uplink code.
|
||||
/// </summary>
|
||||
private void OnGenerateUplinkCode(Entity<RingerUplinkComponent> ent, ref GenerateUplinkCodeEvent ev)
|
||||
{
|
||||
EnsureComp<ActiveRingerComponent>(uid);
|
||||
// Generate a new uplink code
|
||||
var code = GenerateRingtone();
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), uid, Filter.Pvs(uid, 0.05f), false, PopupType.Small);
|
||||
// Set the code on the component
|
||||
ent.Comp.Code = code;
|
||||
|
||||
UpdateRingerUserInterface(uid, ringer, true);
|
||||
// Return the code via the event
|
||||
ev.Code = code;
|
||||
}
|
||||
|
||||
public void RingerPlayRingtone(Entity<RingerComponent?> ent)
|
||||
/// <inheritdoc/>
|
||||
public override bool TryToggleUplink(EntityUid uid, Note[] ringtone, EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
|
||||
return false;
|
||||
|
||||
EnsureComp<ActiveRingerComponent>(ent);
|
||||
if (!HasComp<StoreComponent>(uid))
|
||||
return false;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), ent, Filter.Pvs(ent, 0.05f), false, PopupType.Medium);
|
||||
// On the server, we always check if the code matches
|
||||
if (!uplink.Code.SequenceEqual(ringtone))
|
||||
return false;
|
||||
|
||||
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;
|
||||
|
||||
var ev = new BeforeRingtoneSetEvent(args.Ringtone);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
if (ev.Handled)
|
||||
return;
|
||||
|
||||
UpdateRingerRingtone(uid, ringer, args.Ringtone);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return ToggleUplinkInternal((uid, uplink));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks the uplink and closes the window, if its open
|
||||
/// Generates a random ringtone using the C pentatonic scale.
|
||||
/// </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;
|
||||
|
||||
uplink.Unlocked = false;
|
||||
_ui.CloseUi(uid, StoreUiKey.Key);
|
||||
}
|
||||
|
||||
public void RandomizeRingtone(EntityUid uid, RingerComponent ringer, MapInitEvent args)
|
||||
{
|
||||
UpdateRingerRingtone(uid, ringer, GenerateRingtone());
|
||||
}
|
||||
|
||||
public void RandomizeUplinkCode(EntityUid uid, RingerUplinkComponent uplink, ComponentInit args)
|
||||
{
|
||||
uplink.Code = GenerateRingtone();
|
||||
}
|
||||
|
||||
//Non Event Functions
|
||||
|
||||
/// <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.
|
||||
@@ -169,6 +99,12 @@ namespace Content.Server.PDA.Ringer
|
||||
});
|
||||
}
|
||||
|
||||
/// <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];
|
||||
@@ -180,74 +116,16 @@ namespace Content.Server.PDA.Ringer
|
||||
|
||||
return ringtone;
|
||||
}
|
||||
|
||||
private bool UpdateRingerRingtone(EntityUid uid, RingerComponent ringer, Note[] ringtone)
|
||||
{
|
||||
// Assume validation has already happened.
|
||||
ringer.Ringtone = ringtone;
|
||||
UpdateRingerUserInterface(uid, ringer, HasComp<ActiveRingerComponent>(uid));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateRingerUserInterface(EntityUid uid, RingerComponent ringer, bool isPlaying)
|
||||
{
|
||||
_ui.SetUiState(uid, RingerUiKey.Key, new RingerUpdateState(isPlaying, ringer.Ringtone));
|
||||
}
|
||||
|
||||
public bool ToggleRingerUI(EntityUid uid, EntityUid actor)
|
||||
{
|
||||
_ui.TryToggleUi(uid, RingerUiKey.Key, actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime) //Responsible for actually playing the ringtone
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSound(Note note)
|
||||
{
|
||||
return new ResPath("/Audio/Effects/RingtoneNotes/" + note.ToString().ToLower()) + ".ogg";
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
57
Content.Shared/PDA/Ringer/RingerComponent.cs
Normal file
57
Content.Shared/PDA/Ringer/RingerComponent.cs
Normal file
@@ -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;
|
||||
}
|
||||
@@ -1,26 +1,17 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.PDA.Ringer
|
||||
namespace Content.Shared.PDA.Ringer;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RingerPlayRingtoneMessage : BoundUserInterfaceMessage;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RingerSetRingtoneMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RingerRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RingerPlayRingtoneMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RingerSetRingtoneMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public Note[] Ringtone { get; }
|
||||
|
||||
public RingerSetRingtoneMessage(Note[] ringTone)
|
||||
{
|
||||
Ringtone = ringTone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
Content.Shared/PDA/Ringer/RingerUplinkComponent.cs
Normal file
24
Content.Shared/PDA/Ringer/RingerUplinkComponent.cs
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <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
|
||||
{
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
softness: 5
|
||||
autoRot: true
|
||||
- type: Ringer
|
||||
- type: RingerUplink
|
||||
- type: DeviceNetwork
|
||||
deviceNetId: Wireless
|
||||
receiveFrequencyId: PDA
|
||||
|
||||
Reference in New Issue
Block a user