* OH YES

* хули мне хедкодеру
# Conflicts:
#	Content.Server/Instruments/InstrumentComponent.cs
#	Content.Server/Instruments/InstrumentSystem.cs
#	Content.Shared/Cuffs/SharedCuffableSystem.cs
#	Content.Shared/White/WhiteCVars.cs
#	Resources/Prototypes/tags.yml
This commit is contained in:
rhailrake
2023-05-25 02:13:10 +06:00
committed by Remuchi
parent 10609a2506
commit 484c8a8d9b
24 changed files with 939 additions and 6 deletions

View File

@@ -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<KnockedDownComponent>(true, Player);
}
}
*/

View File

@@ -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<BloodstreamComponent>();
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<BloodstreamComponent> entity, ref ComponentInit args)

View File

@@ -20,6 +20,8 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent
public ICommonSession? InstrumentPlayer =>
_entMan.GetComponentOrNull<ActivatableUIComponent>(Owner)?.CurrentSingleUser
?? _entMan.GetComponentOrNull<ActorComponent>(Owner)?.PlayerSession;
public TimeSpan? TimeStartedPlaying { get; set; }
}
[RegisterComponent]

View File

@@ -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<MetaDataComponent>();
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<ActiveInstrumentComponent>();
var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
var transformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var query = AllEntityQuery<ActiveInstrumentComponent, InstrumentComponent>();
@@ -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);
}

View File

@@ -0,0 +1,11 @@
namespace Content.Server.White.EndOfRoundStats.BloodLost;
public sealed class BloodLostStatEvent : EntityEventArgs
{
public float BloodLost;
public BloodLostStatEvent(float bloodLost)
{
BloodLost = bloodLost;
}
}

View File

@@ -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<BloodLostStatEvent>(OnBloodLost);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void OnBloodLost(BloodLostStatEvent args)
{
totalBloodLost += args.BloodLost;
}
private void OnRoundEnd(RoundEndTextAppendEvent ev)
{
var line = String.Empty;
if (totalBloodLost < _config.GetCVar<float>(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;
}
}

View File

@@ -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<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(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();
}
}

View File

@@ -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} <stat> <color?>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var _stats = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<CommandStatSystem>();
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("<Stat>");
}
if (args.Length == 2)
{
var options = Color.GetAllDefaultColors().Select(o => new CompletionOption(o.Key));
return CompletionResult.FromHintOptions(options, "<Color?>");
}
return CompletionResult.Empty;
}
}

View File

@@ -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<IEntitySystemManager>().GetEntitySystem<CommandStatSystem>();
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}");
}
}
}

View File

@@ -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} <stat index?>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var _stats = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<CommandStatSystem>();
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<IEntitySystemManager>().GetEntitySystem<CommandStatSystem>();
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;
}
}

View File

@@ -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<PlayerData, TimeSpan> _userPlayStats = new();
private struct PlayerData
{
public string Name;
public string? Username;
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CuffableComponent, CuffedTimeStatEvent>(OnUncuffed);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void OnUncuffed(EntityUid uid, CuffableComponent component, CuffedTimeStatEvent args)
{
string? username = null;
if (EntityManager.TryGetComponent<MindComponent>(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<CuffableComponent>();
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();
}
}

View File

@@ -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<SoundSources, int> 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-<Enum> (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<EmitSoundStatEvent>(OnSoundEmitted);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void OnSoundEmitted(EmitSoundStatEvent ev)
{
SoundSources? source = null;
foreach (var enumSource in Enum.GetValues<SoundSources>())
{
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<int>(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-<enum>)");
throw new ArgumentException("Unknown messageId: " + preLocalString);
}
line = localString;
return true;
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
soundsEmitted.Clear();
}
}

View File

@@ -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;
}
}

View File

@@ -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<PlayerData, TimeSpan> userPlayStats = new();
private struct PlayerData
{
public String Name;
public String? Username;
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InstrumentPlayedStatEvent>(OnInstrumentPlayed);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(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<InstrumentComponent>().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();
}
}

View File

@@ -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<GunComponent, AmmoShotEvent>(OnShotFired);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(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<bool>(WhiteCVars.ShotsFiredDisplayNone))
return Loc.GetString("eorstats-shotsfired-noshotsfired");
if (shotsFired == 0 || shotsFired < _config.GetCVar<int>(WhiteCVars.ShotsFiredThreshold))
return string.Empty;
return Loc.GetString("eorstats-shotsfired-amount", ("shotsFired", shotsFired));
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)
{
shotsFired = 0;
}
}

View File

@@ -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<PlayerData, int> _userSlipStats = new();
private struct PlayerData
{
public string Name;
public string? Username;
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SlipperyComponent, SlipEvent>(OnSlip);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
}
private void OnSlip(EntityUid uid, SlipperyComponent slipComp, ref SlipEvent args)
{
string? username = null;
var entity = args.Slipped;
if (EntityManager.TryGetComponent<MindComponent>(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();
}
}

View File

@@ -39,6 +39,9 @@ public sealed partial class CuffableComponent : Component
/// </summary>
[DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)]
public bool CanStillInteract = true;
[DataField("cuffedTime")]
public TimeSpan? CuffedTime { get; set; }
}
[Serializable, NetSerializable]

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.White.EndOfRoundStats.CuffedTime;
public sealed class CuffedTimeStatEvent : EntityEventArgs
{
public TimeSpan Duration;
public CuffedTimeStatEvent(TimeSpan duration)
{
Duration = duration;
}
}

View File

@@ -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;
}
}

View File

@@ -171,7 +171,6 @@ public sealed class WhiteCVars
public static readonly CVarDef<float> JukeboxVolume =
CVarDef.Create("white.jukebox_volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* Chat
*/
@@ -181,4 +180,80 @@ public sealed class WhiteCVars
public static readonly CVarDef<string> DefaultChatSize =
CVarDef.Create("white.chat_size_default", "300;500", CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* End of round stats
*/
/// <summary>
/// The amount of blood lost required to trigger the BloodLost end of round stat.
/// </summary>
/// <remarks>
/// Setting this to 0 will disable the BloodLost end of round stat.
/// </remarks>
public static readonly CVarDef<float> BloodLostThreshold =
CVarDef.Create("eorstats.bloodlost_threshold", 100f, CVar.SERVERONLY);
/// <summary>
/// The amount of time required to trigger the CuffedTime end of round stat, in minutes.
/// </summary>
/// <remarks>
/// Setting this to 0 will disable the CuffedTime end of round stat.
/// </remarks>
public static readonly CVarDef<int> CuffedTimeThreshold =
CVarDef.Create("eorstats.cuffedtime_threshold", 5, CVar.SERVERONLY);
/// <summary>
/// The amount of sounds required to trigger the EmitSound end of round stat.
/// </summary>
/// <remarks>
/// Setting this to 0 will disable the EmitSound end of round stat.
/// </remarks>
public static readonly CVarDef<int> EmitSoundThreshold =
CVarDef.Create("eorstats.emitsound_threshold", 10, CVar.SERVERONLY);
/// <summary>
/// The amount of instruments required to trigger the InstrumentPlayed end of round stat, in minutes.
/// </summary>
/// <remarks>
/// Setting this to 0 will disable the InstrumentPlayed end of round stat.
/// </remarks>
public static readonly CVarDef<int> InstrumentPlayedThreshold =
CVarDef.Create("eorstats.instrumentplayed_threshold", 4, CVar.SERVERONLY);
/// <summary>
/// The amount of shots fired required to trigger the ShotsFired end of round stat.
/// </summary>
/// <remarks>
/// Setting this to 0 will disable the ShotsFired end of round stat.
/// </remarks>
public static readonly CVarDef<int> ShotsFiredThreshold =
CVarDef.Create("eorstats.shotsfired_threshold", 40, CVar.SERVERONLY);
/// <summary>
/// Should a stat be displayed specifically when no shots were fired?
/// </summary>
public static readonly CVarDef<bool> ShotsFiredDisplayNone =
CVarDef.Create("eorstats.shotsfired_displaynone", true, CVar.SERVERONLY);
/// <summary>
/// The amount of times slipped required to trigger the SlippedCount end of round stat.
/// </summary>
/// <remarks>
/// Setting this to 0 will disable the SlippedCount end of round stat.
/// </remarks>
public static readonly CVarDef<int> SlippedCountThreshold =
CVarDef.Create("eorstats.slippedcount_threshold", 30, CVar.SERVERONLY);
/// <summary>
/// Should a stat be displayed specifically when nobody was done?
/// </summary>
public static readonly CVarDef<bool> SlippedCountDisplayNone =
CVarDef.Create("eorstats.slippedcount_displaynone", true, CVar.SERVERONLY);
/// <summary>
/// Should the top slipper be displayed in the end of round stats?
/// </summary>
public static readonly CVarDef<bool> SlippedCountTopSlipper =
CVarDef.Create("eorstats.slippedcount_topslipper", true, CVar.SERVERONLY);
}

View File

@@ -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} раз!

View File

@@ -32,6 +32,9 @@
Cloth: 100
- type: StaticPrice
price: 5
- type: Tag
tags:
- Plushie
- type: entity
parent: BasePlushie