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:
@@ -1,253 +1,131 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.PDA.Ringer;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Server.Audio;
|
||||
|
||||
namespace Content.Server.PDA.Ringer
|
||||
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!;
|
||||
[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!;
|
||||
base.Initialize();
|
||||
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _lastSetRingtoneAt = new();
|
||||
SubscribeLocalEvent<RingerComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// General Event Subscriptions
|
||||
SubscribeLocalEvent<RingerComponent, MapInitEvent>(RandomizeRingtone);
|
||||
SubscribeLocalEvent<RingerUplinkComponent, ComponentInit>(RandomizeUplinkCode);
|
||||
// RingerBoundUserInterface Subscriptions
|
||||
SubscribeLocalEvent<RingerComponent, RingerSetRingtoneMessage>(OnSetRingtone);
|
||||
SubscribeLocalEvent<RingerUplinkComponent, BeforeRingtoneSetEvent>(OnSetUplinkRingtone);
|
||||
SubscribeLocalEvent<RingerComponent, RingerPlayRingtoneMessage>(RingerPlayRingtone);
|
||||
SubscribeLocalEvent<RingerComponent, RingerRequestUpdateInterfaceMessage>(UpdateRingerUserInterfaceDriver);
|
||||
|
||||
SubscribeLocalEvent<RingerComponent, CurrencyInsertAttemptEvent>(OnCurrencyInsert);
|
||||
}
|
||||
|
||||
//Event Functions
|
||||
|
||||
private void OnCurrencyInsert(EntityUid uid, RingerComponent ringer, CurrencyInsertAttemptEvent args)
|
||||
{
|
||||
if (!TryComp<RingerUplinkComponent>(uid, out var uplink))
|
||||
{
|
||||
args.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// if the store can be locked, it must be unlocked first before inserting currency. Stops traitor checking.
|
||||
if (!uplink.Unlocked)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void RingerPlayRingtone(EntityUid uid, RingerComponent ringer, RingerPlayRingtoneMessage args)
|
||||
{
|
||||
EnsureComp<ActiveRingerComponent>(uid);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("comp-ringer-vibration-popup"), uid, Filter.Pvs(uid, 0.05f), false, PopupType.Small);
|
||||
|
||||
UpdateRingerUserInterface(uid, ringer, true);
|
||||
}
|
||||
|
||||
public void RingerPlayRingtone(Entity<RingerComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
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";
|
||||
}
|
||||
SubscribeLocalEvent<RingerUplinkComponent, GenerateUplinkCodeEvent>(OnGenerateUplinkCode);
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct BeforeRingtoneSetEvent(Note[] Ringtone, bool Handled = false);
|
||||
/// <summary>
|
||||
/// Randomizes a ringtone for <see cref="RingerComponent"/> on <see cref="MapInitEvent"/>.
|
||||
/// </summary>
|
||||
private void OnMapInit(Entity<RingerComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
UpdateRingerRingtone(ent, GenerateRingtone());
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user