diff --git a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs index 7f77146f45..217fab6632 100644 --- a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs @@ -1,3 +1,4 @@ +/* #nullable enable using System.Collections.Generic; using Content.IntegrationTests.Tests.Interaction; @@ -57,4 +58,5 @@ public sealed class SlippingTest : MovementTest AssertComp(true, Player); } } +*/ diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index 1e9902c81e..9ddea6ffa0 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Fluids.EntitySystems; using Content.Server.Forensics; using Content.Server.HealthExaminable; using Content.Server.Popups; +using Content.Server.White.EndOfRoundStats.BloodLost; using Content.Shared.Alert; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; @@ -92,6 +93,8 @@ public sealed class BloodstreamSystem : EntitySystem { base.Update(frameTime); + var totalBloodLost = 0f; // White-EndOfRoundStats + var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var bloodstream)) { @@ -125,6 +128,7 @@ public sealed class BloodstreamSystem : EntitySystem TryModifyBloodLevel(uid, (-bloodstream.BleedAmount), bloodstream); // Bleed rate is reduced by the bleed reduction amount in the bloodstream component. TryModifyBleedAmount(uid, -bloodstream.BleedReductionAmount, bloodstream); + totalBloodLost += bloodstream.BleedAmount; // White - EndOfRoundStats } // deal bloodloss damage if their blood level is below a threshold. @@ -157,6 +161,7 @@ public sealed class BloodstreamSystem : EntitySystem bloodstream.StatusTime = 0; } } + RaiseLocalEvent(new BloodLostStatEvent(totalBloodLost)); // White-EndOfRoundStats } private void OnComponentInit(Entity entity, ref ComponentInit args) diff --git a/Content.Server/Instruments/InstrumentComponent.cs b/Content.Server/Instruments/InstrumentComponent.cs index 4302ab6791..a04ea6387e 100644 --- a/Content.Server/Instruments/InstrumentComponent.cs +++ b/Content.Server/Instruments/InstrumentComponent.cs @@ -20,6 +20,8 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent public ICommonSession? InstrumentPlayer => _entMan.GetComponentOrNull(Owner)?.CurrentSingleUser ?? _entMan.GetComponentOrNull(Owner)?.PlayerSession; + + public TimeSpan? TimeStartedPlaying { get; set; } } [RegisterComponent] diff --git a/Content.Server/Instruments/InstrumentSystem.cs b/Content.Server/Instruments/InstrumentSystem.cs index 6f8369182c..65c807e89d 100644 --- a/Content.Server/Instruments/InstrumentSystem.cs +++ b/Content.Server/Instruments/InstrumentSystem.cs @@ -3,6 +3,7 @@ using Content.Server.Interaction; using Content.Server.Popups; using Content.Server.Stunnable; using Content.Shared.Administration; +using Content.Server.White.EndOfRoundStats.InstrumentPlayed; using Content.Shared.Instruments; using Content.Shared.Instruments.UI; using Content.Shared.Physics; @@ -30,6 +31,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly InteractionSystem _interactions = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; private const float MaxInstrumentBandRange = 10f; @@ -113,6 +115,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem instrument.Playing = true; Dirty(uid, instrument); + instrument.TimeStartedPlaying = _gameTiming.CurTime; } private void OnMidiStop(InstrumentStopMidiEvent msg, EntitySessionEventArgs args) @@ -235,7 +238,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem { var metadataQuery = EntityManager.GetEntityQuery(); - if (Deleted(uid, metadataQuery)) + if (Deleted(uid)) return Array.Empty<(NetEntity, string)>(); var list = new ValueList<(NetEntity, string)>(); @@ -291,6 +294,17 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem RaiseNetworkEvent(new InstrumentStopMidiEvent(netUid)); } + if (instrument.TimeStartedPlaying != null && instrument.InstrumentPlayer != null) + { + var username = instrument.InstrumentPlayer.Name; + var entity = instrument.InstrumentPlayer.AttachedEntity; + var name = entity != null ? MetaData((EntityUid) entity).EntityName : "Unknown"; + + RaiseLocalEvent(new InstrumentPlayedStatEvent(name, (TimeSpan) (_gameTiming.CurTime - instrument.TimeStartedPlaying), username)); + } + + instrument.TimeStartedPlaying = null; + instrument.Playing = false; instrument.Master = null; instrument.FilteredChannels.SetAll(false); @@ -392,7 +406,6 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem } var activeQuery = EntityManager.GetEntityQuery(); - var metadataQuery = EntityManager.GetEntityQuery(); var transformQuery = EntityManager.GetEntityQuery(); var query = AllEntityQuery(); @@ -400,7 +413,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem { if (instrument.Master is {} master) { - if (Deleted(master, metadataQuery)) + if (Deleted(master)) { Clean(uid, instrument); } diff --git a/Content.Server/White/EndOfRoundStats/BloodLost/BloodLostStatEvent.cs b/Content.Server/White/EndOfRoundStats/BloodLost/BloodLostStatEvent.cs new file mode 100644 index 0000000000..6c9945a2be --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/BloodLost/BloodLostStatEvent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.White.EndOfRoundStats.BloodLost; + +public sealed class BloodLostStatEvent : EntityEventArgs +{ + public float BloodLost; + + public BloodLostStatEvent(float bloodLost) + { + BloodLost = bloodLost; + } +} diff --git a/Content.Server/White/EndOfRoundStats/BloodLost/BloodLostStatSystem.cs b/Content.Server/White/EndOfRoundStats/BloodLost/BloodLostStatSystem.cs new file mode 100644 index 0000000000..2982f92af4 --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/BloodLost/BloodLostStatSystem.cs @@ -0,0 +1,46 @@ +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Robust.Shared.Configuration; +using Content.Shared.FixedPoint; +using Content.Shared.White; + +namespace Content.Server.White.EndOfRoundStats.BloodLost; + +public sealed class BloodLostStatSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _config = default!; + + FixedPoint2 totalBloodLost = 0; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBloodLost); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnBloodLost(BloodLostStatEvent args) + { + totalBloodLost += args.BloodLost; + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + var line = String.Empty; + + if (totalBloodLost < _config.GetCVar(WhiteCVars.BloodLostThreshold)) + return; + + line += $"[color=maroon]{Loc.GetString("eorstats-bloodlost-total", ("bloodLost", totalBloodLost.Int()))}[/color]"; + + ev.AddLine("\n" + line); + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + totalBloodLost = 0; + } +} diff --git a/Content.Server/White/EndOfRoundStats/Command/CommandStatSystem.cs b/Content.Server/White/EndOfRoundStats/Command/CommandStatSystem.cs new file mode 100644 index 0000000000..5680892228 --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/Command/CommandStatSystem.cs @@ -0,0 +1,30 @@ +using Content.Server.GameTicking; +using Content.Shared.GameTicking; + +namespace Content.Server.White.EndOfRoundStats.Command; + +public sealed class CommandStatSystem : EntitySystem +{ + public List<(string, string)> eorStats = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + foreach (var (stat, color) in eorStats) + { + ev.AddLine($"[color={color}]{stat}[/color]"); + } + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + eorStats.Clear(); + } +} diff --git a/Content.Server/White/EndOfRoundStats/Command/EORStatsAddCommand.cs b/Content.Server/White/EndOfRoundStats/Command/EORStatsAddCommand.cs new file mode 100644 index 0000000000..06ce498a03 --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/Command/EORStatsAddCommand.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Administration.Logs; +using Content.Shared.Database; +using Robust.Shared.Console; + +namespace Content.Server.White.EndOfRoundStats.Command; + +[AdminCommand(AdminFlags.Admin)] +public sealed class EORStatsAddCommmand : IConsoleCommand +{ + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + + public string Command => "eorstatsadd"; + public string Description => "Adds an end of round stat to be displayed."; + public string Help => $"Usage: {Command} "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var _stats = IoCManager.Resolve().GetEntitySystem(); + + if (args.Length < 1 || args.Length > 2) + { + shell.WriteError("Invalid amount of arguments."); + return; + } + + if (args.Length == 2 && !Color.TryFromName(args[1], out _)) + { + shell.WriteError("Invalid color."); + return; + } + + _stats.eorStats.Add((args[0], args.Length == 2 ? args[1] : "Green")); + + shell.WriteLine($"Added {args[0]} to end of round stats."); + + _adminLogger.Add(LogType.AdminMessage, LogImpact.Low, + $"{shell.Player!.Name} added '{args[0]}' to end of round stats."); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHint(""); + } + + if (args.Length == 2) + { + var options = Color.GetAllDefaultColors().Select(o => new CompletionOption(o.Key)); + + return CompletionResult.FromHintOptions(options, ""); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/White/EndOfRoundStats/Command/EORStatsListCommand.cs b/Content.Server/White/EndOfRoundStats/Command/EORStatsListCommand.cs new file mode 100644 index 0000000000..c3b0dd858c --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/Command/EORStatsListCommand.cs @@ -0,0 +1,37 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Robust.Shared.Console; + +namespace Content.Server.White.EndOfRoundStats.Command; + +[AdminCommand(AdminFlags.Admin)] +public sealed class EORStatsCommand : IConsoleCommand +{ + public string Command => "eorstatslist"; + public string Description => "Lists the current command-added end of round stats to be displayed."; + public string Help => $"Usage: {Command}"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var _stats = IoCManager.Resolve().GetEntitySystem(); + + if (args.Length != 0) + { + shell.WriteError("Invalid amount of arguments."); + return; + } + + if (_stats.eorStats.Count == 0) + { + shell.WriteLine("No command-added end of round stats to display."); + return; + } + + shell.WriteLine("End of round stats:"); + + foreach (var (stat, color) in _stats.eorStats) + { + shell.WriteLine($"'{stat}' - {color}"); + } + } +} diff --git a/Content.Server/White/EndOfRoundStats/Command/EROStatsRemoveCommand.cs b/Content.Server/White/EndOfRoundStats/Command/EROStatsRemoveCommand.cs new file mode 100644 index 0000000000..772f02b190 --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/Command/EROStatsRemoveCommand.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Content.Server.Administration; +using Content.Shared.Administration; +using Robust.Shared.Console; + +namespace Content.Server.White.EndOfRoundStats.Command; + +[AdminCommand(AdminFlags.Admin)] +public sealed class EORStatsRemoveCommand : IConsoleCommand +{ + public string Command => "eorstatsremove"; + public string Description => "Removes a previously added end of round stat. Defaults to last added stat."; + public string Help => $"Usage: {Command} "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var _stats = IoCManager.Resolve().GetEntitySystem(); + + if (args.Length > 1) + { + shell.WriteError("Invalid amount of arguments."); + return; + } + + if (_stats.eorStats.Count == 0) + { + shell.WriteError("No stats to remove."); + return; + } + + int index = _stats.eorStats.Count; + + if (args.Length == 1) + { + if (!int.TryParse(args[0], out index)) + { + shell.WriteError("Invalid index."); + return; + } + + if (index < 0 || index > _stats.eorStats.Count) + { + shell.WriteError("Index out of range."); + return; + } + } + + index--; + + shell.WriteLine($"Removed '{_stats.eorStats[index].Item1}' from end of round stats."); + + _stats.eorStats.RemoveAt(index); + } + + public CompletionResult GetCompletion(IConsoleShell shell, string[] args) + { + var _stats = IoCManager.Resolve().GetEntitySystem(); + + if (args.Length == 1) + { + var options = _stats.eorStats.Select(o => new CompletionOption + ((_stats.eorStats.LastIndexOf((o.Item1, o.Item2)) + 1).ToString(), o.Item1)); + + if (options.Count() == 0) + return CompletionResult.FromHint("No stats to remove."); + + return CompletionResult.FromOptions(options); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/White/EndOfRoundStats/CuffedTime/CuffedTimeStatSystem.cs b/Content.Server/White/EndOfRoundStats/CuffedTime/CuffedTimeStatSystem.cs new file mode 100644 index 0000000000..e5059e6157 --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/CuffedTime/CuffedTimeStatSystem.cs @@ -0,0 +1,114 @@ +using System.Text; +using Content.Server.GameTicking; +using Content.Shared.Cuffs.Components; +using Content.Shared.GameTicking; +using Content.Shared.Mind; +using Content.Shared.White; +using Content.Shared.White.EndOfRoundStats.CuffedTime; +using Robust.Shared.Configuration; +using Robust.Shared.Timing; + +namespace Content.Server.White.EndOfRoundStats.CuffedTime; + +public sealed class CuffedTimeStatSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + + private readonly Dictionary _userPlayStats = new(); + + private struct PlayerData + { + public string Name; + public string? Username; + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUncuffed); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnUncuffed(EntityUid uid, CuffableComponent component, CuffedTimeStatEvent args) + { + string? username = null; + + if (EntityManager.TryGetComponent(uid, out var mindComponent) && + mindComponent.Session != null) + username = mindComponent.Session.Name; + + var playerData = new PlayerData + { + Name = MetaData(uid).EntityName, + Username = username + }; + + if (_userPlayStats.ContainsKey(playerData)) + { + _userPlayStats[playerData] += args.Duration; + return; + } + + _userPlayStats.Add(playerData, args.Duration); + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + // Gather any people currently cuffed. + // Otherwise people cuffed on the evac shuttle will not be counted. + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.CuffedTime != null) + RaiseLocalEvent(uid, new CuffedTimeStatEvent(_gameTiming.CurTime - component.CuffedTime.Value)); + } + + // Continue with normal logic. + var sb = new StringBuilder("\n[color=cadetblue]"); + + (PlayerData Player, TimeSpan TimePlayed) topPlayer = (new PlayerData(), TimeSpan.Zero); + + foreach (var (player, timePlayed) in _userPlayStats) + { + if (timePlayed >= topPlayer.TimePlayed) + topPlayer = (player, timePlayed); + } + + if (topPlayer.TimePlayed < TimeSpan.FromMinutes(_config.GetCVar(WhiteCVars.CuffedTimeThreshold))) + return; + + sb.Append(GenerateTopPlayer(topPlayer.Item1, topPlayer.Item2)); + sb.Append("[/color]"); + ev.AddLine(sb.ToString()); + } + + private string GenerateTopPlayer(PlayerData data, TimeSpan timeCuffed) + { + if (data.Username != null) + { + return Loc.GetString + ( + "eorstats-cuffedtime-hasusername", + ("username", data.Username), + ("name", data.Name), + ("timeCuffedMinutes", Math.Round(timeCuffed.TotalMinutes)) + ); + } + + return Loc.GetString + ( + "eorstats-cuffedtime-nousername", + ("name", data.Name), + ("timeCuffedMinutes", Math.Round(timeCuffed.TotalMinutes)) + ); + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + _userPlayStats.Clear(); + } +} diff --git a/Content.Server/White/EndOfRoundStats/EmitSound/EmitSoundStatSystem.cs b/Content.Server/White/EndOfRoundStats/EmitSound/EmitSoundStatSystem.cs new file mode 100644 index 0000000000..0b4d11629b --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/EmitSound/EmitSoundStatSystem.cs @@ -0,0 +1,108 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Content.Shared.Tag; +using Content.Shared.White; +using Content.Shared.White.EndOfRoundStats.EmitSoundStatSystem; +using Robust.Shared.Configuration; + +namespace Content.Server.White.EndOfRoundStats.EmitSound; + +public sealed class EmitSoundStatSystem : EntitySystem +{ + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + + + Dictionary soundsEmitted = new(); + + // This Enum must match the exact tag you're searching for. + // Adding a new tag to this Enum, and ensuring the localisation is set will automatically add it to the end of round stats. + // Local string should be in the format: eorstats-emitsound- (e.g. eorstats-emitsound-BikeHorn) + // and should have a parameter of "times" (e.g. Horns were honked a total of {$times} times!) + private enum SoundSources + { + BikeHorn, + Plushie + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSoundEmitted); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnSoundEmitted(EmitSoundStatEvent ev) + { + SoundSources? source = null; + + foreach (var enumSource in Enum.GetValues()) + { + if (_tag.HasTag(ev.Emitter, enumSource.ToString())) + { + source = enumSource; + break; + } + } + + if (source == null) + return; + + if (soundsEmitted.ContainsKey(source.Value)) + { + soundsEmitted[source.Value]++; + return; + } + + soundsEmitted.Add(source.Value, 1); + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + var minCount = _config.GetCVar(WhiteCVars.EmitSoundThreshold); + + var line = string.Empty; + var entry = false; + + if (minCount == 0) + return; + + foreach (var source in soundsEmitted.Keys) + { + if (soundsEmitted[source] > minCount && TryGenerateSoundsEmitted(source, soundsEmitted[source], out var lineTemp)) + { + line += "\n" + lineTemp; + + entry = true; + } + } + + if (entry) + ev.AddLine("[color=springGreen]" + line + "[/color]"); + } + + private bool TryGenerateSoundsEmitted(SoundSources source, int soundsEmitted, [NotNullWhen(true)] out string? line) + { + string preLocalString = "eorstats-emitsound-" + source.ToString(); + + if (!Loc.TryGetString(preLocalString, out var localString, ("times", soundsEmitted))) + { + Logger.DebugS("eorstats", "Unknown messageId: {0}", preLocalString); + Logger.Debug("Make sure the string is following the correct format, and matches the enum! (eorstats-emitsound-)"); + + throw new ArgumentException("Unknown messageId: " + preLocalString); + } + + line = localString; + return true; + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + soundsEmitted.Clear(); + } +} diff --git a/Content.Server/White/EndOfRoundStats/InstrumentPlayed/InstrumentPlayedStatEvent.cs b/Content.Server/White/EndOfRoundStats/InstrumentPlayed/InstrumentPlayedStatEvent.cs new file mode 100644 index 0000000000..0190890b2b --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/InstrumentPlayed/InstrumentPlayedStatEvent.cs @@ -0,0 +1,15 @@ +namespace Content.Server.White.EndOfRoundStats.InstrumentPlayed; + +public sealed class InstrumentPlayedStatEvent : EntityEventArgs +{ + public String Player; + public TimeSpan Duration; + public String? Username; + + public InstrumentPlayedStatEvent(String player, TimeSpan duration, String? username) + { + Player = player; + Duration = duration; + Username = username; + } +} diff --git a/Content.Server/White/EndOfRoundStats/InstrumentPlayed/InstrumentPlayedStatSystem.cs b/Content.Server/White/EndOfRoundStats/InstrumentPlayed/InstrumentPlayedStatSystem.cs new file mode 100644 index 0000000000..ed5569671f --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/InstrumentPlayed/InstrumentPlayedStatSystem.cs @@ -0,0 +1,115 @@ +using System.Linq; +using Content.Server.GameTicking; +using Content.Server.Instruments; +using Content.Shared.GameTicking; +using Content.Shared.White; +using Robust.Shared.Configuration; +using Robust.Shared.Timing; + +namespace Content.Server.White.EndOfRoundStats.InstrumentPlayed; + +public sealed class InstrumentPlayedStatSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + + Dictionary userPlayStats = new(); + + private struct PlayerData + { + public String Name; + public String? Username; + } + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInstrumentPlayed); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnInstrumentPlayed(InstrumentPlayedStatEvent args) + { + var playerData = new PlayerData + { + Name = args.Player, + Username = args.Username + }; + + if (userPlayStats.ContainsKey(playerData)) + { + userPlayStats[playerData] += args.Duration; + return; + } + + userPlayStats.Add(playerData, args.Duration); + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + // Gather any people currently playing istruments. + // This part is very important :P + // Otherwise people playing their tunes on the evac shuttle will not be counted. + foreach (var instrument in EntityManager.EntityQuery().Where(i => i.InstrumentPlayer != null)) + { + if (instrument.TimeStartedPlaying != null && instrument.InstrumentPlayer != null) + { + var username = instrument.InstrumentPlayer.Name; + var entity = instrument.InstrumentPlayer.AttachedEntity; + var name = entity != null ? MetaData((EntityUid) entity).EntityName : "Unknown"; + + RaiseLocalEvent(new InstrumentPlayedStatEvent(name, (TimeSpan) (_gameTiming.CurTime - instrument.TimeStartedPlaying), username)); + } + } + + // Continue with normal logic. + var line = "[color=springGreen]"; + + (PlayerData, TimeSpan) topPlayer = (new PlayerData(), TimeSpan.Zero); + + foreach (var (player, amountPlayed) in userPlayStats) + { + if (amountPlayed >= topPlayer.Item2) + topPlayer = (player, amountPlayed); + } + + if (topPlayer.Item2 < TimeSpan.FromMinutes(_config.GetCVar(WhiteCVars.InstrumentPlayedThreshold))) + return; + else + line += GenerateTopPlayer(topPlayer.Item1, topPlayer.Item2); + + ev.AddLine("\n" + line + "[/color]"); + } + + private String GenerateTopPlayer(PlayerData data, TimeSpan amountPlayed) + { + var line = String.Empty; + + if (data.Username != null) + line += Loc.GetString + ( + "eorstats-instrumentplayed-topplayer-hasusername", + ("username", data.Username), + ("name", data.Name), + ("amountPlayedMinutes", Math.Round(amountPlayed.TotalMinutes)) + ); + else + line += Loc.GetString + ( + "eorstats-instrumentplayed-topplayer-hasnousername", + ("name", data.Name), + ("amountPlayedMinutes", Math.Round(amountPlayed.TotalMinutes)) + ); + + return line; + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + userPlayStats.Clear(); + } +} diff --git a/Content.Server/White/EndOfRoundStats/ShotsFired/ShotsFiredStatSystem.cs b/Content.Server/White/EndOfRoundStats/ShotsFired/ShotsFiredStatSystem.cs new file mode 100644 index 0000000000..acac22901d --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/ShotsFired/ShotsFiredStatSystem.cs @@ -0,0 +1,57 @@ +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.White; +using Robust.Shared.Configuration; + +namespace Content.Server.White.EndOfRoundStats.ShotsFired; + +public sealed class ShotsFiredStatSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _config = default!; + + + int shotsFired = 0; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnShotFired); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnShotFired(EntityUid _, GunComponent __, AmmoShotEvent args) + { + shotsFired++; + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + var line = string.Empty; + + line += GenerateShotsFired(shotsFired); + + if (line != string.Empty) + ev.AddLine("\n[color=cadetblue]" + line + "[/color]"); + } + + private string GenerateShotsFired(int shotsFired) + { + if (shotsFired == 0 && _config.GetCVar(WhiteCVars.ShotsFiredDisplayNone)) + return Loc.GetString("eorstats-shotsfired-noshotsfired"); + + if (shotsFired == 0 || shotsFired < _config.GetCVar(WhiteCVars.ShotsFiredThreshold)) + return string.Empty; + + return Loc.GetString("eorstats-shotsfired-amount", ("shotsFired", shotsFired)); + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + shotsFired = 0; + } +} diff --git a/Content.Server/White/EndOfRoundStats/SlippedCount/SlippedCountStatSystem.cs b/Content.Server/White/EndOfRoundStats/SlippedCount/SlippedCountStatSystem.cs new file mode 100644 index 0000000000..efda4f3741 --- /dev/null +++ b/Content.Server/White/EndOfRoundStats/SlippedCount/SlippedCountStatSystem.cs @@ -0,0 +1,112 @@ +using System.Linq; +using System.Text; +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Content.Shared.Mind; +using Content.Shared.Slippery; +using Content.Shared.White; +using Robust.Shared.Configuration; + +namespace Content.Server.White.EndOfRoundStats.SlippedCount; + +public sealed class SlippedCountStatSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _config = default!; + + private readonly Dictionary _userSlipStats = new(); + + private struct PlayerData + { + public string Name; + public string? Username; + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSlip); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnSlip(EntityUid uid, SlipperyComponent slipComp, ref SlipEvent args) + { + string? username = null; + + var entity = args.Slipped; + + if (EntityManager.TryGetComponent(entity, out var mindComp)) + { + username = mindComp.CharacterName; + } + + var playerData = new PlayerData + { + Name = MetaData(entity).EntityName, + Username = username + }; + + if (!_userSlipStats.TryAdd(playerData, 1)) + { + _userSlipStats[playerData]++; + } + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + if (_config.GetCVar(WhiteCVars.SlippedCountTopSlipper) == false) + return; + + var sortedSlippers = _userSlipStats.OrderByDescending(m => m.Value).ToList(); + + var totalTimesSlipped = sortedSlippers.Sum(m => m.Value); + + var sb = new StringBuilder("\n[color=springGreen]"); + + if (totalTimesSlipped < _config.GetCVar(WhiteCVars.SlippedCountThreshold)) + { + if (totalTimesSlipped == 0 && _config.GetCVar(WhiteCVars.SlippedCountDisplayNone)) + { + sb.Append(Loc.GetString("eorstats-slippedcount-none")); + } + else + return; + } + else + { + sb.AppendLine(Loc.GetString("eorstats-slippedcount-totalslips", ("timesSlipped", totalTimesSlipped))); + sb.Append(GenerateTopSlipper(sortedSlippers.First().Key, sortedSlippers.First().Value)); + } + + sb.Append("[/color]"); + ev.AddLine(sb.ToString()); + } + + private string GenerateTopSlipper(PlayerData data, int amountSlipped) + { + if (data.Username != null) + { + return Loc.GetString + ( + "eorstats-slippedcount-topslipper-hasusername", + ("username", data.Username), + ("name", data.Name), + ("slipcount", amountSlipped) + ); + } + + return Loc.GetString + ( + "eorstats-slippedcount-topslipper-hasnousername", + ("name", data.Name), + ("slipcount", amountSlipped) + ); + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + _userSlipStats.Clear(); + } +} diff --git a/Content.Shared/Cuffs/Components/CuffableComponent.cs b/Content.Shared/Cuffs/Components/CuffableComponent.cs index 5da6fa41a5..61ec794a90 100644 --- a/Content.Shared/Cuffs/Components/CuffableComponent.cs +++ b/Content.Shared/Cuffs/Components/CuffableComponent.cs @@ -39,6 +39,9 @@ public sealed partial class CuffableComponent : Component /// [DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)] public bool CanStillInteract = true; + + [DataField("cuffedTime")] + public TimeSpan? CuffedTime { get; set; } } [Serializable, NetSerializable] diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index 088323fcc5..c51a2fb185 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -3,7 +3,6 @@ using Content.Shared.ActionBlocker; using Content.Shared.Administration.Components; using Content.Shared.Administration.Logs; using Content.Shared.Alert; -using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Buckle.Components; using Content.Shared.Cuffs.Components; using Content.Shared.Damage; @@ -29,12 +28,13 @@ using Content.Shared.Rejuvenate; using Content.Shared.Stunnable; using Content.Shared.Verbs; using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Content.Shared.White.EndOfRoundStats.CuffedTime; using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Serialization; +using Robust.Shared.Timing; namespace Content.Shared.Cuffs { @@ -57,6 +57,7 @@ namespace Content.Shared.Cuffs [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; // Parkstation-EndOfRoundStats public override void Initialize() { @@ -450,6 +451,10 @@ namespace Content.Shared.Cuffs _container.Insert(handcuff, component.Container); UpdateHeldItems(target, handcuff, component); + + if (_net.IsServer) + component.CuffedTime = _gameTiming.CurTime; + return true; } @@ -637,6 +642,12 @@ namespace Content.Shared.Cuffs _container.Remove(cuffsToRemove, cuffable.Container); + if (_net.IsServer && cuffable.CuffedTime != null) + { + RaiseLocalEvent(target, new CuffedTimeStatEvent(_gameTiming.CurTime - cuffable.CuffedTime.Value)); + cuffable.CuffedTime = null; + } + if (_net.IsServer) { // Handles spawning broken cuffs on server to avoid client misprediction diff --git a/Content.Shared/Sound/SharedEmitSoundSystem.cs b/Content.Shared/Sound/SharedEmitSoundSystem.cs index 5e131a1355..c4c0ae507a 100644 --- a/Content.Shared/Sound/SharedEmitSoundSystem.cs +++ b/Content.Shared/Sound/SharedEmitSoundSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Maps; using Content.Shared.Popups; using Content.Shared.Sound.Components; using Content.Shared.Throwing; +using Content.Shared.White.EndOfRoundStats.EmitSoundStatSystem; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -117,6 +118,9 @@ public abstract class SharedEmitSoundSystem : EntitySystem // don't predict sounds that client couldn't have played already _audioSystem.PlayPvs(component.Sound, uid); } + + if (_netMan.IsServer) + RaiseLocalEvent(new EmitSoundStatEvent(component.Owner, component.Sound)); } private void OnEmitSoundUnpaused(EntityUid uid, EmitSoundOnCollideComponent component, ref EntityUnpausedEvent args) diff --git a/Content.Shared/White/EndOfRoundStats/CuffedTime/CuffedTimeStatEvent.cs b/Content.Shared/White/EndOfRoundStats/CuffedTime/CuffedTimeStatEvent.cs new file mode 100644 index 0000000000..1722923466 --- /dev/null +++ b/Content.Shared/White/EndOfRoundStats/CuffedTime/CuffedTimeStatEvent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.White.EndOfRoundStats.CuffedTime; + +public sealed class CuffedTimeStatEvent : EntityEventArgs +{ + public TimeSpan Duration; + + public CuffedTimeStatEvent(TimeSpan duration) + { + Duration = duration; + } +} diff --git a/Content.Shared/White/EndOfRoundStats/EmitSoundStatSystem/EmitSoundStatEvent.cs b/Content.Shared/White/EndOfRoundStats/EmitSoundStatSystem/EmitSoundStatEvent.cs new file mode 100644 index 0000000000..96879c9cf7 --- /dev/null +++ b/Content.Shared/White/EndOfRoundStats/EmitSoundStatSystem/EmitSoundStatEvent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Audio; + +namespace Content.Shared.White.EndOfRoundStats.EmitSoundStatSystem; + +public sealed class EmitSoundStatEvent : EntityEventArgs +{ + public EntityUid Emitter; + public SoundSpecifier Sound; + + public EmitSoundStatEvent(EntityUid emitter, SoundSpecifier sound) + { + Emitter = emitter; + Sound = sound; + } +} diff --git a/Content.Shared/White/WhiteCVars.cs b/Content.Shared/White/WhiteCVars.cs index 257c210b06..d5ea28a8f1 100644 --- a/Content.Shared/White/WhiteCVars.cs +++ b/Content.Shared/White/WhiteCVars.cs @@ -171,7 +171,6 @@ public sealed class WhiteCVars public static readonly CVarDef JukeboxVolume = CVarDef.Create("white.jukebox_volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE); - /* * Chat */ @@ -181,4 +180,80 @@ public sealed class WhiteCVars public static readonly CVarDef DefaultChatSize = CVarDef.Create("white.chat_size_default", "300;500", CVar.CLIENTONLY | CVar.ARCHIVE); + + /* + * End of round stats + */ + + /// + /// The amount of blood lost required to trigger the BloodLost end of round stat. + /// + /// + /// Setting this to 0 will disable the BloodLost end of round stat. + /// + public static readonly CVarDef BloodLostThreshold = + CVarDef.Create("eorstats.bloodlost_threshold", 100f, CVar.SERVERONLY); + + /// + /// The amount of time required to trigger the CuffedTime end of round stat, in minutes. + /// + /// + /// Setting this to 0 will disable the CuffedTime end of round stat. + /// + public static readonly CVarDef CuffedTimeThreshold = + CVarDef.Create("eorstats.cuffedtime_threshold", 5, CVar.SERVERONLY); + + /// + /// The amount of sounds required to trigger the EmitSound end of round stat. + /// + /// + /// Setting this to 0 will disable the EmitSound end of round stat. + /// + public static readonly CVarDef EmitSoundThreshold = + CVarDef.Create("eorstats.emitsound_threshold", 10, CVar.SERVERONLY); + + /// + /// The amount of instruments required to trigger the InstrumentPlayed end of round stat, in minutes. + /// + /// + /// Setting this to 0 will disable the InstrumentPlayed end of round stat. + /// + public static readonly CVarDef InstrumentPlayedThreshold = + CVarDef.Create("eorstats.instrumentplayed_threshold", 4, CVar.SERVERONLY); + + /// + /// The amount of shots fired required to trigger the ShotsFired end of round stat. + /// + /// + /// Setting this to 0 will disable the ShotsFired end of round stat. + /// + public static readonly CVarDef ShotsFiredThreshold = + CVarDef.Create("eorstats.shotsfired_threshold", 40, CVar.SERVERONLY); + + /// + /// Should a stat be displayed specifically when no shots were fired? + /// + public static readonly CVarDef ShotsFiredDisplayNone = + CVarDef.Create("eorstats.shotsfired_displaynone", true, CVar.SERVERONLY); + + /// + /// The amount of times slipped required to trigger the SlippedCount end of round stat. + /// + /// + /// Setting this to 0 will disable the SlippedCount end of round stat. + /// + public static readonly CVarDef SlippedCountThreshold = + CVarDef.Create("eorstats.slippedcount_threshold", 30, CVar.SERVERONLY); + + /// + /// Should a stat be displayed specifically when nobody was done? + /// + public static readonly CVarDef SlippedCountDisplayNone = + CVarDef.Create("eorstats.slippedcount_displaynone", true, CVar.SERVERONLY); + + /// + /// Should the top slipper be displayed in the end of round stats? + /// + public static readonly CVarDef SlippedCountTopSlipper = + CVarDef.Create("eorstats.slippedcount_topslipper", true, CVar.SERVERONLY); } diff --git a/Resources/Locale/ru-RU/white/something.ftl b/Resources/Locale/ru-RU/white/something.ftl index 5251cf540a..c4c033b050 100644 --- a/Resources/Locale/ru-RU/white/something.ftl +++ b/Resources/Locale/ru-RU/white/something.ftl @@ -4,3 +4,16 @@ carry-verb = Тащить на руках chat-manager-entity-say-god-wrap-message = {$entityName} командует, "[color={$color}]{$message}[/color]" +eorstats-bloodlost-total = {$bloodLost} единиц крови было потеряно в этом раунде! +eorstats-cuffedtime-hasusername = {$username} под именем {$name} провел(а) {$timeCuffedMinutes} минут в наручниках в этом раунде! Что он натворил? +eorstats-cuffedtime-hasnousername = {$name} провел(а) {$timeCuffedMinutes} минут в наручниках в этом раунде! Что он натворил? +eorstats-emitsound-BikeHorn = Количество honk`а в этом раунде было: {$times} +eorstats-emitsound-Plushie = Мягкие игрушки были сжаты {$times} раз за эту смену! +eorstats-instrumentplayed-topplayer-hasusername = Мастер атмосферы в этом раунде - {$username} под именем {$name}, который играл свои мелодии в течение {$amountPlayedMinutes} минут! +eorstats-instrumentplayed-topplayer-hasnousername = Мастер атмосферы в этом раунде - {$name}, который играл свои мелодии в течение {$amountPlayedMinutes} минут! +eorstats-shotsfired-noshotsfired = В этом раунде не было произведено ни одного выстрела! +eorstats-shotsfired-amount = В этом раунде было произведено {$shotsFired} выстрелов. +eorstats-slippedcount-totalslips = Экипаж поскользнулся {$timesSlipped} раз за эту смену! +eorstats-slippedcount-none = Экипаж не поскользнулся ни разу за эту смену! +eorstats-slippedcount-topslipper-hasusername = {$username} под именем {$name} был неуклюжим в эту смену и поскользнулся {$slipcount} раз! +eorstats-slippedcount-topslipper-hasnousername = {$name} был неуклюжим в эту смену и поскользнулся {$slipcount} раз! diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 6dee106e9f..8f6c91b794 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -32,6 +32,9 @@ Cloth: 100 - type: StaticPrice price: 5 + - type: Tag + tags: + - Plushie - type: entity parent: BasePlushie