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
|
namespace Content.Client.PDA.Ringer
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class RingerBoundUserInterface : BoundUserInterface
|
public sealed class RingerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||||
{
|
{
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private RingtoneMenu? _menu;
|
private RingtoneMenu? _menu;
|
||||||
|
|
||||||
public RingerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Open()
|
protected override void Open()
|
||||||
{
|
{
|
||||||
base.Open();
|
base.Open();
|
||||||
_menu = this.CreateWindow<RingtoneMenu>();
|
_menu = this.CreateWindow<RingtoneMenu>();
|
||||||
_menu.OpenToLeft();
|
_menu.OpenToLeft();
|
||||||
|
|
||||||
_menu.TestRingerButton.OnPressed += _ =>
|
_menu.TestRingtoneButtonPressed += OnTestRingtoneButtonPressed;
|
||||||
{
|
_menu.SetRingtoneButtonPressed += OnSetRingtoneButtonPressed;
|
||||||
SendMessage(new RingerPlayRingtoneMessage());
|
|
||||||
};
|
|
||||||
|
|
||||||
_menu.SetRingerButton.OnPressed += _ =>
|
Update();
|
||||||
{
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetRingtone(out Note[] ringtone)
|
private bool TryGetRingtone(out Note[] ringtone)
|
||||||
@@ -63,36 +44,59 @@ namespace Content.Client.PDA.Ringer
|
|||||||
return true;
|
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;
|
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))
|
||||||
if (RingtoneMenu.IsNote(note))
|
continue;
|
||||||
{
|
|
||||||
_menu.PreviousNoteInputs[i] = note.Replace("sharp", "#");
|
|
||||||
_menu.RingerNoteInputs[i].Text = _menu.PreviousNoteInputs[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
_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()
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
if (_menu is null)
|
||||||
if (!disposing)
|
|
||||||
return;
|
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"
|
||||||
Title="{Loc 'comp-ringer-ui-menu-title'}"
|
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||||
MinSize="320 128"
|
Title="{Loc 'comp-ringer-ui-menu-title'}"
|
||||||
SetSize="320 128">
|
MinSize="320 100"
|
||||||
|
SetSize="320 100">
|
||||||
<BoxContainer Orientation="Vertical"
|
<BoxContainer Orientation="Vertical"
|
||||||
VerticalExpand="True"
|
VerticalExpand="True"
|
||||||
HorizontalExpand="True"
|
HorizontalExpand="True"
|
||||||
@@ -90,4 +91,4 @@
|
|||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</DefaultWindow>
|
</controls:FancyWindow>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Content.Client.UserInterface.Controls;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface.CustomControls;
|
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Content.Shared.PDA;
|
using Content.Shared.PDA;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
@@ -8,15 +8,21 @@ using Robust.Client.UserInterface.Controls;
|
|||||||
namespace Content.Client.PDA.Ringer
|
namespace Content.Client.PDA.Ringer
|
||||||
{
|
{
|
||||||
[GenerateTypedNameReferences]
|
[GenerateTypedNameReferences]
|
||||||
public sealed partial class RingtoneMenu : DefaultWindow
|
public sealed partial class RingtoneMenu : FancyWindow
|
||||||
{
|
{
|
||||||
public string[] PreviousNoteInputs = new[] { "A", "A", "A", "A", "A", "A" };
|
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()
|
public RingtoneMenu()
|
||||||
{
|
{
|
||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
|
SetRingerButton.OnPressed += _ => SetRingtoneButtonPressed?.Invoke();
|
||||||
|
TestRingerButton.OnPressed += _ => TestRingtoneButtonPressed?.Invoke();
|
||||||
|
|
||||||
RingerNoteInputs = new[] { RingerNoteOneInput, RingerNoteTwoInput, RingerNoteThreeInput, RingerNoteFourInput, RingerNoteFiveInput, RingerNoteSixInput };
|
RingerNoteInputs = new[] { RingerNoteOneInput, RingerNoteTwoInput, RingerNoteThreeInput, RingerNoteFourInput, RingerNoteFiveInput, RingerNoteSixInput };
|
||||||
|
|
||||||
for (var i = 0; i < RingerNoteInputs.Length; ++i)
|
for (var i = 0; i < RingerNoteInputs.Length; ++i)
|
||||||
@@ -43,14 +49,28 @@ namespace Content.Client.PDA.Ringer
|
|||||||
foo();
|
foo();
|
||||||
input.CursorPosition = input.Text.Length; // Resets caret position to the end of the typed input
|
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");
|
input.AddStyleClass("Caution");
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
PreviousNoteInputs[index] = newText;
|
||||||
input.RemoveStyleClass("Caution");
|
input.RemoveStyleClass("Caution");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update if there's a change
|
||||||
|
if (newText != input.Text)
|
||||||
|
input.Text = newText;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,13 +184,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
{
|
{
|
||||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA");
|
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink is PDA");
|
||||||
// Codes are only generated if the uplink is a 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
|
if (ev.Code is { } generatedCode)
|
||||||
briefing = string.Format("{0}\n{1}",
|
{
|
||||||
briefing,
|
code = generatedCode;
|
||||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
|
||||||
return (code, briefing);
|
// 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)
|
else if (pda is null && uplinked)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,15 +10,16 @@ using Content.Server.Traitor.Uplink;
|
|||||||
using Content.Shared.Access.Components;
|
using Content.Shared.Access.Components;
|
||||||
using Content.Shared.CartridgeLoader;
|
using Content.Shared.CartridgeLoader;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
|
using Content.Shared.DeviceNetwork.Components;
|
||||||
using Content.Shared.Light;
|
using Content.Shared.Light;
|
||||||
using Content.Shared.Light.EntitySystems;
|
using Content.Shared.Light.EntitySystems;
|
||||||
using Content.Shared.PDA;
|
using Content.Shared.PDA;
|
||||||
|
using Content.Shared.PDA.Ringer;
|
||||||
using Robust.Server.Containers;
|
using Robust.Server.Containers;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using Content.Shared.DeviceNetwork.Components;
|
|
||||||
|
|
||||||
namespace Content.Server.PDA
|
namespace Content.Server.PDA
|
||||||
{
|
{
|
||||||
@@ -166,7 +167,7 @@ namespace Content.Server.PDA
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send new UI state to clients, call if you modify something like uplink.
|
/// Send new UI state to clients, call if you modify something like uplink.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null)
|
public override void UpdatePdaUi(EntityUid uid, PdaComponent? pda = null)
|
||||||
{
|
{
|
||||||
if (!Resolve(uid, ref pda, false))
|
if (!Resolve(uid, ref pda, false))
|
||||||
return;
|
return;
|
||||||
@@ -243,7 +244,7 @@ namespace Content.Server.PDA
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (HasComp<RingerComponent>(uid))
|
if (HasComp<RingerComponent>(uid))
|
||||||
_ringer.ToggleRingerUI(uid, msg.Actor);
|
_ringer.TryToggleRingerUi(uid, msg.Actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowMusicMessage msg)
|
private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaShowMusicMessage msg)
|
||||||
@@ -272,7 +273,7 @@ namespace Content.Server.PDA
|
|||||||
|
|
||||||
if (TryComp<RingerUplinkComponent>(uid, out var uplink))
|
if (TryComp<RingerUplinkComponent>(uid, out var uplink))
|
||||||
{
|
{
|
||||||
_ringer.LockUplink(uid, uplink);
|
_ringer.LockUplink((uid, uplink));
|
||||||
UpdatePdaUi(uid, pda);
|
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,253 +1,131 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Content.Server.Store.Components;
|
|
||||||
using Content.Server.Store.Systems;
|
using Content.Server.Store.Systems;
|
||||||
using Content.Shared.PDA;
|
using Content.Shared.PDA;
|
||||||
using Content.Shared.PDA.Ringer;
|
using Content.Shared.PDA.Ringer;
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Store;
|
|
||||||
using Content.Shared.Store.Components;
|
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.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 IRobustRandom _random = default!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
[Dependency] private readonly PdaSystem _pda = default!;
|
base.Initialize();
|
||||||
[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();
|
SubscribeLocalEvent<RingerComponent, MapInitEvent>(OnMapInit);
|
||||||
|
SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
|
||||||
|
|
||||||
public override void Initialize()
|
SubscribeLocalEvent<RingerUplinkComponent, GenerateUplinkCodeEvent>(OnGenerateUplinkCode);
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Note[] GenerateRingtone(Note[] notes)
|
|
||||||
{
|
|
||||||
var ringtone = new Note[RingtoneLength];
|
|
||||||
|
|
||||||
for (var i = 0; i < RingtoneLength; i++)
|
|
||||||
{
|
|
||||||
ringtone[i] = _random.Pick(notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
/// <summary>
|
||||||
public record struct BeforeRingtoneSetEvent(Note[] Ringtone, bool Handled = false);
|
/// Randomizes a ringtone for <see cref="RingerComponent"/> on <see cref="MapInitEvent"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void OnMapInit(Entity<RingerComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the store can be locked, it must be unlocked first before inserting currency. Stops traitor checking.
|
||||||
|
if (!uplink.Unlocked)
|
||||||
|
args.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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();
|
||||||
|
|
||||||
|
// Set the code on the component
|
||||||
|
ent.Comp.Code = code;
|
||||||
|
|
||||||
|
// Return the code via the event
|
||||||
|
ev.Code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
|
||||||
|
// On the server, we always check if the code matches
|
||||||
|
if (!uplink.Code.SequenceEqual(ringtone))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ToggleUplinkInternal((uid, uplink));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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];
|
||||||
|
|
||||||
|
for (var i = 0; i < RingtoneLength; i++)
|
||||||
|
{
|
||||||
|
ringtone[i] = _random.Pick(notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ringtone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 System.Linq;
|
||||||
using Content.Server.Actions;
|
using Content.Server.Actions;
|
||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
using Content.Server.PDA.Ringer;
|
|
||||||
using Content.Server.Stack;
|
using Content.Server.Stack;
|
||||||
using Content.Server.Store.Components;
|
using Content.Server.Store.Components;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
@@ -9,6 +8,7 @@ using Content.Shared.Database;
|
|||||||
using Content.Shared.FixedPoint;
|
using Content.Shared.FixedPoint;
|
||||||
using Content.Shared.Hands.EntitySystems;
|
using Content.Shared.Hands.EntitySystems;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.PDA.Ringer;
|
||||||
using Content.Shared.Store;
|
using Content.Shared.Store;
|
||||||
using Content.Shared.Store.Components;
|
using Content.Shared.Store.Components;
|
||||||
using Content.Shared.UserInterface;
|
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;
|
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
|
||||||
{
|
{
|
||||||
|
public Note[] Ringtone { get; }
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
public RingerSetRingtoneMessage(Note[] ringTone)
|
||||||
public sealed class RingerRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
|
|
||||||
{
|
{
|
||||||
}
|
Ringtone = ringTone;
|
||||||
|
|
||||||
[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;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.PDA.Ringer
|
namespace Content.Shared.PDA.Ringer;
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public enum RingerUiKey
|
|
||||||
{
|
|
||||||
Key
|
|
||||||
}
|
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum RingerUiKey : byte
|
||||||
|
{
|
||||||
|
Key,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,5 +66,11 @@ namespace Content.Shared.PDA
|
|||||||
{
|
{
|
||||||
Appearance.SetData(uid, PdaVisuals.IdCardInserted, pda.ContainedId != null);
|
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.Serialization;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Shared.PDA;
|
namespace Content.Shared.PDA;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the shared functionality for PDA ringtones.
|
||||||
|
/// </summary>
|
||||||
public abstract class SharedRingerSystem : EntitySystem
|
public abstract class SharedRingerSystem : EntitySystem
|
||||||
{
|
{
|
||||||
public const int RingtoneLength = 6;
|
public const int RingtoneLength = 6;
|
||||||
public const int NoteTempo = 300;
|
public const int NoteTempo = 300;
|
||||||
public const float NoteDelay = 60f / NoteTempo;
|
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]
|
[Serializable, NetSerializable]
|
||||||
public enum Note : byte
|
public enum Note : byte
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
softness: 5
|
softness: 5
|
||||||
autoRot: true
|
autoRot: true
|
||||||
- type: Ringer
|
- type: Ringer
|
||||||
|
- type: RingerUplink
|
||||||
- type: DeviceNetwork
|
- type: DeviceNetwork
|
||||||
deviceNetId: Wireless
|
deviceNetId: Wireless
|
||||||
receiveFrequencyId: PDA
|
receiveFrequencyId: PDA
|
||||||
|
|||||||
Reference in New Issue
Block a user