diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 4e6035d17a..e17b3a6a2f 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -60,7 +60,7 @@ namespace Content.Client.Entry [Dependency] private readonly DocumentParsingManager _documentParsingManager = default!; [Dependency] private readonly GhostKickManager _ghostKick = default!; [Dependency] private readonly ExtendedDisconnectInformationManager _extendedDisconnectInformation = default!; - [Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!; + [Dependency] private readonly JobRequirementsManager _jobRequirements = default!; [Dependency] private readonly ContentLocalizationManager _contentLoc = default!; public override void Init() @@ -130,7 +130,7 @@ namespace Content.Client.Entry _viewportManager.Initialize(); _ghostKick.Initialize(); _extendedDisconnectInformation.Initialize(); - _playTimeTracking.Initialize(); + _jobRequirements.Initialize(); //AUTOSCALING default Setup! _configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080); diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index e8c22ad028..c7d45ec085 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -42,7 +42,7 @@ namespace Content.Client.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); } } diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index b2d8240644..0d9d72480a 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -22,7 +22,7 @@ namespace Content.Client.LateJoin [Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystem = default!; - [Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!; + [Dependency] private readonly JobRequirementsManager _jobRequirements = default!; public event Action<(EntityUid, string)> SelectedId; @@ -54,6 +54,7 @@ namespace Content.Client.LateJoin Contents.AddChild(_base); + _jobRequirements.Updated += RebuildUI; RebuildUI(); SelectedId += x => @@ -249,7 +250,7 @@ namespace Content.Client.LateJoin jobButton.OnPressed += _ => SelectedId.Invoke((id, jobButton.JobId)); - if (!_playTimeTracking.IsAllowed(prototype, out var reason)) + if (!_jobRequirements.IsAllowed(prototype, out var reason)) { jobButton.Disabled = true; @@ -289,6 +290,7 @@ namespace Content.Client.LateJoin if (disposing) { + _jobRequirements.Updated -= RebuildUI; _gameTicker.LobbyJobsAvailableUpdated -= JobsAvailableUpdated; _jobButtons.Clear(); _jobCategories.Clear(); diff --git a/Content.Client/Players/PlayTimeTracking/PlayTimeTrackingManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs similarity index 68% rename from Content.Client/Players/PlayTimeTracking/PlayTimeTrackingManager.cs rename to Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 3f13c2983b..25a0061610 100644 --- a/Content.Client/Players/PlayTimeTracking/PlayTimeTrackingManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text; using Content.Shared.CCVar; +using Content.Shared.Players; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Roles; using Robust.Client; @@ -11,7 +13,7 @@ using Robust.Shared.Prototypes; namespace Content.Client.Players.PlayTimeTracking; -public sealed class PlayTimeTrackingManager +public sealed class JobRequirementsManager { [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly IClientNetManager _net = default!; @@ -20,9 +22,18 @@ public sealed class PlayTimeTrackingManager [Dependency] private readonly IPrototypeManager _prototypes = default!; private readonly Dictionary _roles = new(); + private readonly List _roleBans = new(); + + private ISawmill _sawmill = default!; + + public event Action? Updated; public void Initialize() { + _sawmill = Logger.GetSawmill("job_requirements"); + + // Yeah the client manager handles role bans and playtime but the server ones are separate DEAL. + _net.RegisterNetMessage(RxRoleBans); _net.RegisterNetMessage(RxPlayTime); _client.RunLevelChanged += ClientOnRunLevelChanged; @@ -37,6 +48,18 @@ public sealed class PlayTimeTrackingManager } } + private void RxRoleBans(MsgRoleBans message) + { + _sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries."); + + if (_roleBans.Equals(message.Bans)) + return; + + _roleBans.Clear(); + _roleBans.AddRange(message.Bans); + Updated?.Invoke(); + } + private void RxPlayTime(MsgPlayTime message) { _roles.Clear(); @@ -52,27 +75,36 @@ public sealed class PlayTimeTrackingManager { sawmill.Info($"{tracker}: {time}"); }*/ + Updated?.Invoke(); } public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out string? reason) { reason = null; + if (_roleBans.Contains($"Job:{job.ID}")) + { + reason = Loc.GetString("role-ban"); + return false; + } + if (job.Requirements == null || !_cfg.GetCVar(CCVars.GameRoleTimers)) + { return true; + } var player = _playerManager.LocalPlayer?.Session; - if (player == null) return true; + if (player == null) + return true; - var roles = _roles; var reasonBuilder = new StringBuilder(); var first = true; foreach (var requirement in job.Requirements) { - if (JobRequirements.TryRequirementMet(requirement, roles, out reason, _prototypes)) + if (JobRequirements.TryRequirementMet(requirement, _roles, out reason, _prototypes)) continue; if (!first) diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs index b404b13fcf..1c50644a42 100644 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs @@ -53,6 +53,7 @@ namespace Content.Client.Preferences.UI private readonly IEntityManager _entMan; private readonly IConfigurationManager _configurationManager; private readonly MarkingManager _markingManager; + private readonly JobRequirementsManager _requirements; private LineEdit _ageEdit => CAgeEdit; private LineEdit _nameEdit => CNameEdit; @@ -377,96 +378,9 @@ namespace Content.Client.Preferences.UI _jobPriorities = new List(); _jobCategories = new Dictionary(); - - var firstCategory = true; - var playTime = IoCManager.Resolve(); - - foreach (var department in _prototypeManager.EnumeratePrototypes()) - { - var departmentName = Loc.GetString($"department-{department.ID}"); - - if (!_jobCategories.TryGetValue(department.ID, out var category)) - { - category = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Name = department.ID, - ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip", - ("departmentName", departmentName)) - }; - - if (firstCategory) - { - firstCategory = false; - } - else - { - category.AddChild(new Control - { - MinSize = new Vector2(0, 23), - }); - } - - category.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")}, - Children = - { - new Label - { - Text = Loc.GetString("humanoid-profile-editor-department-jobs-label", - ("departmentName", departmentName)), - Margin = new Thickness(5f, 0, 0, 0) - } - } - }); - - _jobCategories[department.ID] = category; - _jobList.AddChild(category); - } - - var jobs = department.Roles.Select(o => _prototypeManager.Index(o)).Where(o => o.SetPreference).ToList(); - jobs.Sort((x, y) => -string.Compare(x.LocalizedName, y.LocalizedName, StringComparison.CurrentCultureIgnoreCase)); - - foreach (var job in jobs) - { - var selector = new JobPrioritySelector(job); - - if (!playTime.IsAllowed(job, out var reason)) - { - selector.LockRequirements(reason); - } - - category.AddChild(selector); - _jobPriorities.Add(selector); - - selector.PriorityChanged += priority => - { - Profile = Profile?.WithJobPriority(job.ID, priority); - IsDirty = true; - - foreach (var jobSelector in _jobPriorities) - { - // Sync other selectors with the same job in case of multiple department jobs - if (jobSelector.Job == selector.Job) - { - jobSelector.Priority = priority; - } - - // Lower any other high priorities to medium. - if (priority == JobPriority.High) - { - if (jobSelector.Job != selector.Job && jobSelector.Priority == JobPriority.High) - { - jobSelector.Priority = JobPriority.Medium; - Profile = Profile?.WithJobPriority(jobSelector.Job.ID, JobPriority.Medium); - } - } - } - }; - - } - } + _requirements = IoCManager.Resolve(); + _requirements.Updated += UpdateRoleRequirements; + UpdateRoleRequirements(); #endregion Jobs @@ -603,6 +517,101 @@ namespace Content.Client.Preferences.UI IsDirty = false; } + private void UpdateRoleRequirements() + { + _jobList.DisposeAllChildren(); + _jobPriorities.Clear(); + _jobCategories.Clear(); + var firstCategory = true; + + foreach (var department in _prototypeManager.EnumeratePrototypes()) + { + var departmentName = Loc.GetString($"department-{department.ID}"); + + if (!_jobCategories.TryGetValue(department.ID, out var category)) + { + category = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Name = department.ID, + ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip", + ("departmentName", departmentName)) + }; + + if (firstCategory) + { + firstCategory = false; + } + else + { + category.AddChild(new Control + { + MinSize = new Vector2(0, 23), + }); + } + + category.AddChild(new PanelContainer + { + PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")}, + Children = + { + new Label + { + Text = Loc.GetString("humanoid-profile-editor-department-jobs-label", + ("departmentName", departmentName)), + Margin = new Thickness(5f, 0, 0, 0) + } + } + }); + + _jobCategories[department.ID] = category; + _jobList.AddChild(category); + } + + var jobs = department.Roles.Select(o => _prototypeManager.Index(o)).Where(o => o.SetPreference).ToList(); + jobs.Sort((x, y) => -string.Compare(x.LocalizedName, y.LocalizedName, StringComparison.CurrentCultureIgnoreCase)); + + foreach (var job in jobs) + { + var selector = new JobPrioritySelector(job); + + if (!_requirements.IsAllowed(job, out var reason)) + { + selector.LockRequirements(reason); + } + + category.AddChild(selector); + _jobPriorities.Add(selector); + + selector.PriorityChanged += priority => + { + Profile = Profile?.WithJobPriority(job.ID, priority); + IsDirty = true; + + foreach (var jobSelector in _jobPriorities) + { + // Sync other selectors with the same job in case of multiple department jobs + if (jobSelector.Job == selector.Job) + { + jobSelector.Priority = priority; + } + + // Lower any other high priorities to medium. + if (priority == JobPriority.High) + { + if (jobSelector.Job != selector.Job && jobSelector.Priority == JobPriority.High) + { + jobSelector.Priority = JobPriority.Medium; + Profile = Profile?.WithJobPriority(jobSelector.Job.ID, JobPriority.Medium); + } + } + } + }; + + } + } + } + private void OnFlavorTextChange(string content) { if (Profile is null) @@ -694,6 +703,8 @@ namespace Content.Client.Preferences.UI if (_previewDummy != null) _entMan.DeleteEntity(_previewDummy.Value); + var playTime = IoCManager.Resolve(); + playTime.Updated -= UpdateRoleRequirements; _preferencesManager.OnServerDataLoaded -= LoadServerData; } diff --git a/Content.Server/Administration/Commands/DepartmentBanCommand.cs b/Content.Server/Administration/Commands/DepartmentBanCommand.cs index 0bc662acc0..513ea8fc35 100644 --- a/Content.Server/Administration/Commands/DepartmentBanCommand.cs +++ b/Content.Server/Administration/Commands/DepartmentBanCommand.cs @@ -9,6 +9,10 @@ namespace Content.Server.Administration.Commands; [AdminCommand(AdminFlags.Ban)] public sealed class DepartmentBanCommand : IConsoleCommand { + [Dependency] private readonly IPlayerLocator _locater = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly RoleBanManager _bans = default!; + public string Command => "departmentban"; public string Description => Loc.GetString("cmd-departmentban-desc"); public string Help => Loc.GetString("cmd-departmentban-help"); @@ -46,19 +50,25 @@ public sealed class DepartmentBanCommand : IConsoleCommand return; } - var protoManager = IoCManager.Resolve(); - - if (!protoManager.TryIndex(department, out var departmentProto)) + if (!_protoManager.TryIndex(department, out var departmentProto)) { return; } - var banManager = IoCManager.Resolve(); + var located = await _locater.LookupIdByNameOrIdAsync(target); + + if (located == null) + { + shell.WriteError(Loc.GetString("cmd-roleban-name-parse")); + return; + } foreach (var job in departmentProto.Roles) { - banManager.CreateJobBan(shell, target, job, reason, minutes); + _bans.CreateJobBan(shell, located, job, reason, minutes); } + + _bans.SendRoleBans(located); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Commands/RoleBanCommand.cs b/Content.Server/Administration/Commands/RoleBanCommand.cs index 2822ec5fbd..13fde6c114 100644 --- a/Content.Server/Administration/Commands/RoleBanCommand.cs +++ b/Content.Server/Administration/Commands/RoleBanCommand.cs @@ -12,6 +12,9 @@ namespace Content.Server.Administration.Commands; [AdminCommand(AdminFlags.Ban)] public sealed class RoleBanCommand : IConsoleCommand { + [Dependency] private readonly IPlayerLocator _locator = default!; + [Dependency] private readonly RoleBanManager _bans = default!; + public string Command => "roleban"; public string Description => Loc.GetString("cmd-roleban-desc"); public string Help => Loc.GetString("cmd-roleban-help"); @@ -49,7 +52,16 @@ public sealed class RoleBanCommand : IConsoleCommand return; } - IoCManager.Resolve().CreateJobBan(shell, target, job, reason, minutes); + var located = await _locator.LookupIdByNameOrIdAsync(target); + + if (located == null) + { + shell.WriteError(Loc.GetString("cmd-roleban-name-parse")); + return; + } + + _bans.CreateJobBan(shell, located, job, reason, minutes); + _bans.SendRoleBans(located); } public CompletionResult GetCompletion(IConsoleShell shell, string[] args) diff --git a/Content.Server/Administration/Managers/RoleBanManager.cs b/Content.Server/Administration/Managers/RoleBanManager.cs index c4cdc8b01b..f4b136bd66 100644 --- a/Content.Server/Administration/Managers/RoleBanManager.cs +++ b/Content.Server/Administration/Managers/RoleBanManager.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using Content.Server.Database; +using Content.Shared.Players; using Content.Shared.Roles; using Robust.Server.Player; using Robust.Shared.Console; @@ -16,17 +17,21 @@ namespace Content.Server.Administration.Managers; public sealed class RoleBanManager { + [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IPlayerLocator _playerLocator = default!; private const string JobPrefix = "Job:"; + private ISawmill _sawmill = default!; + private readonly Dictionary> _cachedRoleBans = new(); public void Initialize() { + _sawmill = Logger.GetSawmill("rolebans"); + _netManager.RegisterNetMessage(); _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } @@ -34,10 +39,13 @@ public sealed class RoleBanManager { if (e.NewStatus != SessionStatus.Connected || _cachedRoleBans.ContainsKey(e.Session.UserId)) + { return; + } var netChannel = e.Session.ConnectedClient; await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId); + SendRoleBans(e.Session); } private async Task AddRoleBan(ServerRoleBanDef banDef) @@ -49,14 +57,41 @@ public sealed class RoleBanManager roleBans = new HashSet(); _cachedRoleBans.Add(banDef.UserId.Value, roleBans); } - if (!roleBans.Contains(banDef)) - roleBans.Add(banDef); + + roleBans.Add(banDef); } await _db.AddServerRoleBanAsync(banDef); return true; } + public void SendRoleBans(LocatedPlayerData located) + { + if (!_playerManager.TryGetSessionById(located.UserId, out var player)) + { + return; + } + + SendRoleBans(player); + } + + public void SendRoleBans(IPlayerSession pSession) + { + if (!_cachedRoleBans.TryGetValue(pSession.UserId, out var roleBans)) + { + _sawmill.Error($"Tried to send rolebans for {pSession.Name} but none cached?"); + return; + } + + var bans = new MsgRoleBans() + { + Bans = roleBans.Select(o => o.Role).ToList() + }; + + _sawmill.Debug($"Sent rolebans to {pSession.Name}"); + _netManager.ServerSendMessage(bans, pSession.ConnectedClient); + } + public HashSet? GetRoleBans(NetUserId playerUserId) { return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans) ? roleBans.Select(banDef => banDef.Role).ToHashSet() : null; @@ -98,7 +133,7 @@ public sealed class RoleBanManager } #region Job Bans - public async void CreateJobBan(IConsoleShell shell, string target, string job, string reason, uint minutes) + public async void CreateJobBan(IConsoleShell shell, LocatedPlayerData located, string job, string reason, uint minutes) { if (!_prototypeManager.TryIndex(job, out JobPrototype? _)) { @@ -107,7 +142,7 @@ public sealed class RoleBanManager } job = string.Concat(JobPrefix, job); - CreateRoleBan(shell, target, job, reason, minutes); + CreateRoleBan(shell, located, job, reason, minutes); } public HashSet? GetJobBans(NetUserId playerUserId) @@ -122,15 +157,8 @@ public sealed class RoleBanManager #endregion #region Commands - private async void CreateRoleBan(IConsoleShell shell, string target, string role, string reason, uint minutes) + private async void CreateRoleBan(IConsoleShell shell, LocatedPlayerData located, string role, string reason, uint minutes) { - var located = await _playerLocator.LookupIdByNameOrIdAsync(target); - if (located == null) - { - shell.WriteError(Loc.GetString("cmd-roleban-name-parse")); - return; - } - var targetUid = located.UserId; var targetHWid = located.LastHWId; var targetAddress = located.LastAddress; @@ -167,12 +195,12 @@ public sealed class RoleBanManager if (!await AddRoleBan(banDef)) { - shell.WriteLine(Loc.GetString("cmd-roleban-existing", ("target", target), ("role", role))); + shell.WriteLine(Loc.GetString("cmd-roleban-existing", ("target", located.Username), ("role", role))); return; } var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires)); - shell.WriteLine(Loc.GetString("cmd-roleban-success", ("target", target), ("role", role), ("reason", reason), ("length", length))); + shell.WriteLine(Loc.GetString("cmd-roleban-success", ("target", located.Username), ("role", role), ("reason", reason), ("length", length))); } #endregion } diff --git a/Content.Shared/Players/MsgRoleBans.cs b/Content.Shared/Players/MsgRoleBans.cs new file mode 100644 index 0000000000..fd90f62b0b --- /dev/null +++ b/Content.Shared/Players/MsgRoleBans.cs @@ -0,0 +1,36 @@ +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared.Players; + +/// +/// Sent server -> client to inform the client of their role bans. +/// +public sealed class MsgRoleBans : NetMessage +{ + public override MsgGroups MsgGroup => MsgGroups.EntityEvent; + + public List Bans = new(); + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + var count = buffer.ReadVariableInt32(); + Bans.EnsureCapacity(count); + + for (var i = 0; i < count; i++) + { + Bans.Add(buffer.ReadString()); + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + buffer.WriteVariableInt32(Bans.Count); + + foreach (var ban in Bans) + { + buffer.Write(ban); + } + } +} diff --git a/Content.Shared/Players/PlayTimeTracking/MsgPlayTime.cs b/Content.Shared/Players/PlayTimeTracking/MsgPlayTime.cs index 91ba274649..ad057f0792 100644 --- a/Content.Shared/Players/PlayTimeTracking/MsgPlayTime.cs +++ b/Content.Shared/Players/PlayTimeTracking/MsgPlayTime.cs @@ -16,6 +16,8 @@ public sealed class MsgPlayTime : NetMessage public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { var count = buffer.ReadVariableInt32(); + Trackers.EnsureCapacity(count); + for (var i = 0; i < count; i++) { Trackers.Add(buffer.ReadString(), buffer.ReadTimeSpan()); diff --git a/Resources/Locale/en-US/job/role-timers.ftl b/Resources/Locale/en-US/job/role-timers.ftl index c9ebb75ed6..8e70e234d3 100644 --- a/Resources/Locale/en-US/job/role-timers.ftl +++ b/Resources/Locale/en-US/job/role-timers.ftl @@ -6,3 +6,5 @@ role-timer-role-insufficient = You require {TOSTRING($time, "0")} more minutes w role-timer-role-too-high = You require {TOSTRING($time, "0")} fewer minutes with {$job} to play this role. (Are you trying to play a trainee role?) role-timer-locked = Locked (hover for details) + +role-ban = You have been banned from this role.