diff --git a/Content.Server/Backmen/Ghost/Roles/GhostRoleRollerSystem.cs b/Content.Server/Backmen/Ghost/Roles/GhostRoleRollerSystem.cs new file mode 100644 index 00000000000..256b01d4233 --- /dev/null +++ b/Content.Server/Backmen/Ghost/Roles/GhostRoleRollerSystem.cs @@ -0,0 +1,259 @@ +using System.Linq; +using System.Threading; +using Content.Server.Administration.Logs; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; +using Content.Shared.Backmen.CCVar; +using Content.Shared.Chat; +using Content.Shared.Database; +using Content.Shared.GameTicking; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Utility; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.Backmen.Ghost.Roles; + +public sealed class GhostRoleRollerSystem : EntitySystem +{ + private record TimerQueue(TimeSpan Start, TimeSpan DeadLine) + { + public bool IsFinished { get; set; } = false; + public bool IsProcess { get; set; } = false; + public TimeSpan? FinishDate { get; set; } + public NetUserId? FinishUser { get; set; } + public Dictionary Bids { get; } = new(); + public CancellationTokenSource TokenSource { get; } = new(); + } + + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!; + + private ISawmill _sawmill = default!; + + private readonly Dictionary _queues = new(); + private readonly Dictionary> _history = new(); + private readonly HashSet _busy = new(); + + private bool _enabled = false; + private TimeSpan _roleTimer = TimeSpan.FromSeconds(3); + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnCleanup); + _cfg.OnValueChanged(CCVars.GhostRollerTime, f => + { + _roleTimer = TimeSpan.FromSeconds(f); + }, true); + + _cfg.OnValueChanged(CCVars.GhostRollerEnabled, f => + { + _enabled = f; + if(!f) + Cleanup(); + }, true); + + _sawmill = _logManager.GetSawmill("backmen.ghost"); + + _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; + } + + public override void Shutdown() + { + base.Shutdown(); + + _playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged; + } + + private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus is SessionStatus.Disconnected or SessionStatus.Connected) + { + _busy.Remove(e.Session.UserId); + } + } + + private void OnCleanup(RoundRestartCleanupEvent ev) + { + Cleanup(); + } + + private void Cleanup() + { + _queues.Clear(); + _history.Clear(); + _busy.Clear(); + } + + public void RegisterGhostRole(Entity role) + { + if(!_enabled) + return; + + var now = _gameTicker.RoundDuration(); + var q = new TimerQueue(now, now + _roleTimer + TimeSpan.FromSeconds(1)); + if(_queues.TryAdd(role.Comp.Identifier, q)) + Timer.Spawn(_roleTimer, () => EndRoleTimer(role), q.TokenSource.Token); + } + + public void UnregisterGhostRole(Entity role) + { + if(!_enabled) + return; + + if (!_queues.TryGetValue(role.Comp.Identifier, out var queue) || queue.IsFinished) + return; + foreach (var (user, _) in queue.Bids) + { + _busy.Remove(user); + } + queue.TokenSource.Cancel(false); + } + + public void Takeover(Entity role, ref TakeGhostRoleEvent ev) + { + if(!_enabled) + return; + + var now = _gameTicker.RoundDuration(); + + if (_busy.Contains(ev.Player.UserId)) + { + _chatManager.DispatchServerMessage(ev.Player, Loc.GetString("ghostroller-busy"), true); + ev.TookRole = true; + return; + } + + if (TerminatingOrDeleted(role) || + !_queues.TryGetValue(role.Comp.Identifier, out var queue) || + queue.IsFinished || + queue.DeadLine < now || + queue.TokenSource.Token.IsCancellationRequested) + return; + + if (queue.IsProcess) + { + _chatManager.DispatchServerMessage(ev.Player, Loc.GetString("ghostroller-is-process"), true); + ev.TookRole = true; + return; + } + + ev.TookRole = true; + + _busy.Add(ev.Player.UserId); + var bid = _random.Next(0, 100); + queue.Bids.TryAdd(ev.Player.UserId, bid); + _history.TryAdd(ev.Player.UserId, new List()); + _history[ev.Player.UserId].Add(queue); + _chatManager.DispatchServerMessage(ev.Player, Loc.GetString("ghostroller-notify-bid",("entity",role.Owner),("roll",bid)), true); + } + + private void EndRoleTimer(Entity role) + { + if(!_enabled) + return; + + if (!_queues.TryGetValue(role.Comp.Identifier, out var queue)) + { + _sawmill.Error($"queue {role.Comp.Identifier} not exist!"); + return; + } + + try + { + queue.IsProcess = true; + + if (TerminatingOrDeleted(role) || queue.TokenSource.Token.IsCancellationRequested) + { + _sawmill.Error($"queue {role.Comp.Identifier} delete or canceled!"); + if (queue.Bids.Count > 0) + { + SendToAllPlayerInQueue(queue, Loc.GetString("ghostroller-notify-canceled")); + } + + queue.IsFinished = true; + return; + } + + var now = _gameTicker.RoundDuration(); + + startpick: + if (queue.Bids.Count == 0) + { + _sawmill.Error($"queue {role.Comp.Identifier} no binds!"); + queue.IsFinished = true; + return; + } + + var winner = queue.Bids.MaxBy(x => x.Value); + + if (_playerManager.TryGetSessionById(winner.Key, out var player)) + { + + + queue.FinishDate = now; + queue.FinishUser = winner.Key; + + var ev = new TakeGhostRoleEvent(player); + RaiseLocalEvent(role, ref ev); + + if (!ev.TookRole) + { + SendToAllPlayerInQueue(queue, Loc.GetString("ghostroller-cant-be-took")); + return; + } + + + if (player.AttachedEntity != null) + _adminLogger.Add(LogType.GhostRoleTaken, LogImpact.Low, + $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity} with roll {winner.Value}"); + + SendToAllPlayerInQueue(queue, + Loc.GetString("ghostroller-notify-winner", ("name", player.Name), ("entity", role.Owner), ("roll", winner.Value)), true); + } + else + { + queue.Bids.Remove(winner.Key); + goto startpick; //repick + } + + queue.IsFinished = true; + } + finally + { + queue.IsProcess = false; + foreach (var (user, _) in queue.Bids) + { + _busy.Remove(user); + } + } + } + + private void SendToAllPlayerInQueue(TimerQueue queue, string msg, bool cloneBUi = false) + { + var filter = Filter.Empty(); + foreach (var (userId, _) in queue.Bids) + { + if(!_playerManager.TryGetSessionById(userId, out var sess)) + continue; + filter.AddPlayer(sess); + _ghostRoleSystem.CloseEui(sess); + } + var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(msg))); + _chatManager.ChatMessageToManyFiltered( filter, ChatChannel.Server, msg, wrappedMessage, default, false, true, null); + } + +} diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs index 8d94ef1caac..f15cbe2ccc1 100644 --- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs @@ -39,6 +39,7 @@ public sealed class GhostRoleSystem : EntitySystem [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; [Dependency] private readonly Backmen.RoleWhitelist.WhitelistSystem _roleWhitelist = default!; // backmen: whitelist + [Dependency] private readonly Backmen.Ghost.Roles.GhostRoleRollerSystem _roleRoller = default!; // backmen: ghost roller [Dependency] private readonly SharedRoleSystem _roleSystem = default!; private uint _nextRoleIdentifier; @@ -186,6 +187,7 @@ public void RegisterGhostRole(Entity role) return; _ghostRoles[role.Comp.Identifier = GetNextRoleIdentifier()] = role; + _roleRoller.RegisterGhostRole(role); // backmen: ghost roller UpdateAllEui(); } @@ -196,6 +198,7 @@ public void UnregisterGhostRole(Entity role) return; _ghostRoles.Remove(comp.Identifier); + _roleRoller.UnregisterGhostRole(role); // backmen: ghost roller UpdateAllEui(); } @@ -210,6 +213,11 @@ public void Takeover(ICommonSession player, uint identifier) // end-backmen: whitelist var ev = new TakeGhostRoleEvent(player); + // start-backmen: ghost roller + _roleRoller.Takeover(role, ref ev); + if(ev.TookRole) + return; + // end-backmen: ghost roller RaiseLocalEvent(role, ref ev); if (!ev.TookRole) diff --git a/Content.Shared/Backmen/CCVar/CCVars.cs b/Content.Shared/Backmen/CCVar/CCVars.cs index 0cd0890152d..2c2d7967782 100644 --- a/Content.Shared/Backmen/CCVar/CCVars.cs +++ b/Content.Shared/Backmen/CCVar/CCVars.cs @@ -150,4 +150,17 @@ public static readonly CVarDef public static readonly CVarDef BlobPlayersPer = CVarDef.Create("blob.players_per", 20, CVar.SERVERONLY); + + + /* + * enabling a roll to enter a ghost role for one player from the vote + */ + public static readonly CVarDef + GhostRollerEnabled = CVarDef.Create("ghost.roller_enabled", false, CVar.SERVERONLY); + + /// + /// the time that will be given to throw a number to vote for the ghost role + /// + public static readonly CVarDef GhostRollerTime = + CVarDef.Create("ghost.roller_time", 10f, CVar.REPLICATED | CVar.SERVER); } diff --git a/Resources/Locale/ru-RU/backmen/GhostRoleRollerSystem.ftl b/Resources/Locale/ru-RU/backmen/GhostRoleRollerSystem.ftl new file mode 100644 index 00000000000..9117d3b4fed --- /dev/null +++ b/Resources/Locale/ru-RU/backmen/GhostRoleRollerSystem.ftl @@ -0,0 +1,7 @@ + +ghostroller-cant-be-took = Данную роль нельзя взять +ghostroller-notify-winner = В голосовании за { $entity }, победил { $name }, с роллом { $roll } +ghostroller-notify-canceled = Голосование за роль было отменено +ghostroller-busy = Вы уже участвуете в раздаче! +ghostroller-is-process = Раздача уже в процессе! +ghostroller-notify-bid = Вы участвуте за { $entity } с роллом { $roll }