using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.GameTicking;
using Content.Shared.PDA;
using Content.Shared.PDA.Ringer;
using Content.Shared.Store;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.PDA.Ringer;
///
/// Handles the server-side logic for .
///
public sealed partial class RingerSystem : SharedRingerSystem
{
[Dependency] private IRobustRandom _random = default!;
public static Note[] AllowedNotes =
{
Note.C,
Note.D,
Note.E,
Note.F,
Note.G,
Note.A,
Note.B
};
///
/// Stores the serialized version of any ringtone that can be excluded from new ringtone generations.
///
[ViewVariables]
public readonly HashSet ReservedSerializedRingtones = new();
///
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnCurrencyInsert);
SubscribeLocalEvent(OnGenerateUplinkCode);
SubscribeLocalEvent(CleanupReserved);
InitialSetup();
}
///
/// Randomizes a ringtone for on .
///
private void OnMapInit(Entity ent, ref MapInitEvent args)
{
var ringtone = GenerateRingtone();
ringtone ??= new Note[RingtoneLength] { Note.A, Note.A, Note.A, Note.A, Note.A, Note.A }; // Fallback
UpdateRingerRingtone(ent, ringtone);
}
///
/// Handles the for .
///
private void OnCurrencyInsert(Entity ent, ref CurrencyInsertAttemptEvent args)
{
// TODO: Store isn't predicted, can't move it to shared
if (!TryComp(ent, out var uplink))
{
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();
}
///
/// Handles the for generating an uplink code.
///
private void OnGenerateUplinkCode(Entity ent, ref GenerateUplinkCodeEvent ev)
{
var code = GenerateRingtone(true, true);
// Set the code on the component
ent.Comp.Code = code;
// Return the code via the event
ev.Code = code;
}
private void InitialSetup()
{
ReservedSerializedRingtones.Clear();
}
///
public override bool TryToggleUplink(Entity entity, Note[] ringtone, EntityUid? user = null)
{
if (!Resolve(entity, ref entity.Comp))
return false;
// On the server, we always check if the code matches
if (!TryMatchRingtoneToStore(ringtone, out var store, entity))
return false;
// If the store is not this entity, we make sure to properly set the remote store.
if (store != entity.Owner)
Store.SetRemoteStore(entity.Owner, store);
return ToggleUplinkInternal((entity, entity.Comp));
}
///
/// Generates a random ringtone using the C major scale.
///
/// Exclude any ringtone registered to ReservedSerializedRingtones.
/// Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true.
/// An array of Notes representing the ringtone.
/// The logic for this is on the Server so that we don't get a different result on the Client every time.
private Note[]? GenerateRingtone(bool excludeReserved = false, bool reserveRingtone = false)
{
// Default to using C major so it at least sounds not terrible.
return GenerateRingtone(AllowedNotes, excludeReserved, reserveRingtone);
}
///
/// Generates a random ringtone using the specified notes.
///
/// The notes to choose from when generating the ringtone.
/// Exclude any ringtone registered to ReservedSerializedRingtones.
/// Add the generated ringtone to ReservedSerializedRingtones. Requires ExcludeReserved to be true.
/// An array of Notes representing the ringtone.
/// The logic for this is on the Server so that we don't get a different result on the Client every time.
private Note[]? GenerateRingtone(Note[] notes, bool excludeReserved = false, bool reserveRingtone = false)
{
var excludedRingtones = excludeReserved ? ReservedSerializedRingtones.ToArray() : null;
var maxPow = Math.Pow(notes.Length, RingtoneLength);
if (maxPow > int.MaxValue)
{
return null;
}
var generatedRingtone = NextIntInRangeButExclude(0, Convert.ToInt32(maxPow) - 1, excludedRingtones);
if (!TryDeserializeRingtone(notes, generatedRingtone, out var ringtone))
return null;
if (excludeReserved && reserveRingtone)
ReservedSerializedRingtones.Add(generatedRingtone);
return ringtone;
}
///
/// Serialize a ringtone, representing it as an Int32.
///
/// The array of notes used to generate the ringtone.
/// The ringtone which needs to be serialized.
/// The ringtone in a serialized format.
/// Whether the ringtone could be serialized or not.
private bool TrySerializeRingtone(Note[] allowedNotes, Note[] ringtone, [NotNullWhen(true)] out int? serializedRingtone)
{
var noteLength = allowedNotes.Length;
// The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk.
// If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting.
var maxPow = Math.Pow(noteLength, ringtone.Length);
if (maxPow > int.MaxValue)
{
serializedRingtone = null;
return false;
}
var serializationValue = 0;
for (var i = 0; i < ringtone.Length; i++)
{
var pow = Math.Pow(noteLength, i);
var index = Array.IndexOf(allowedNotes, ringtone[i]);
if (index == -1)
{
serializedRingtone = null;
return false;
}
serializationValue += Convert.ToInt32(pow) * index;
}
serializedRingtone = serializationValue;
return true;
}
///
/// Deserialize a serialized ringtone into a Note array.
///
/// The array of notes used to generate the ringtone.
/// The ringtone in a serialized format.
/// The ringtone resulting from the deserialization.
/// Whether the ringtone could be deserialized or not.
private bool TryDeserializeRingtone(Note[] allowedNotes, int serializedRingtone, [NotNullWhen(true)] out Note[]? ringtone)
{
var noteLength = allowedNotes.Length;
ringtone = new Note[RingtoneLength];
// The serialization stores as an Int32, and therefore using Pow risks overshooting the max value, so we check for if that's a risk.
// If using 12 possible notes, you can have a ringtone sequence of 7 notes safely without overshooting.
var maxPow = Math.Pow(noteLength, RingtoneLength);
if (maxPow > int.MaxValue)
{
ringtone = null;
return false;
}
for (var i = 0; i < RingtoneLength; i++)
{
var pow = Math.Pow(noteLength, RingtoneLength - 1 - i);
var powInt = Convert.ToInt32(pow);
var val = serializedRingtone / powInt;
if (!AllowedNotes.TryGetValue(val, out var note))
{
ringtone = null;
return false;
}
ringtone[RingtoneLength - 1 - i] = note;
serializedRingtone -= val * powInt;
}
return true;
}
///
/// Try to get the store entity that has the matching ringer access.
///
/// Notes from the ringer.
/// The store entity, if there is one.
/// The entity providing the code.
public bool TryMatchRingtoneToStore(Note[] notes, [NotNullWhen(true)] out EntityUid? store, EntityUid? ringer = null)
{
var query = EntityQueryEnumerator();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Code != null && notes.SequenceEqual(comp.Code))
{
if (comp.BoundEntity != null && comp.BoundEntity != ringer)
break;
store = uid;
return true;
}
}
store = null;
return false;
}
private void CleanupReserved(RoundRestartCleanupEvent ev)
{
ReservedSerializedRingtones.Clear();
}
private int NextIntInRangeButExclude(int start, int end, int[]? excludes)
{
excludes ??= new int[0];
Array.Sort(excludes);
var rangeLength = end - start - excludes.Length;
var randomInt = _random.Next(rangeLength) + start;
for (var i = 0; i < excludes.Length; i++)
{
if (excludes[i] > randomInt)
{
return randomInt;
}
randomInt++;
}
return randomInt;
}
public void SetBoundUplinkEntity(Entity entity, EntityUid? targetEntity)
{
entity.Comp.BoundEntity = targetEntity;
}
}
///
/// Event raised to generate a new uplink code for a PDA.
///
[ByRefEvent]
public record struct GenerateUplinkCodeEvent
{
///
/// The generated uplink code (filled in by the event handler).
///
public Note[]? Code;
}