Add job whitelist system (#28085)

* Add job whitelist system

* Address reviews

* Fix name

* Apply suggestions from code review

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* cancinium

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
This commit is contained in:
DrSmugleaf
2024-06-01 05:08:31 -07:00
committed by GitHub
parent e3a66136bf
commit 19be94c9ea
35 changed files with 4666 additions and 47 deletions

View File

@@ -22,7 +22,6 @@ using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Client.Lobby; namespace Content.Client.Lobby;
@@ -70,12 +69,9 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_profileEditor?.RefreshFlavorText(); _profileEditor?.RefreshFlavorText();
}); });
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, args => _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
{
_profileEditor?.RefreshAntags(); _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
_profileEditor?.RefreshJobs();
_profileEditor?.RefreshLoadouts();
});
} }
private LobbyCharacterPreviewPanel? GetLobbyPreview() private LobbyCharacterPreviewPanel? GetLobbyPreview()
@@ -193,6 +189,13 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
PreviewPanel.SetSummaryText(humanoid.Summary); PreviewPanel.SetSummaryText(humanoid.Summary);
} }
private void RefreshProfileEditor()
{
_profileEditor?.RefreshAntags();
_profileEditor?.RefreshJobs();
_profileEditor?.RefreshLoadouts();
}
private void SaveProfile() private void SaveProfile()
{ {
DebugTools.Assert(EditedProfile != null); DebugTools.Assert(EditedProfile != null);

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Players; using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist;
using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Client; using Robust.Client;
@@ -24,6 +25,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
private readonly Dictionary<string, TimeSpan> _roles = new(); private readonly Dictionary<string, TimeSpan> _roles = new();
private readonly List<string> _roleBans = new(); private readonly List<string> _roleBans = new();
private readonly List<string> _jobWhitelists = new();
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
@@ -36,6 +38,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
// Yeah the client manager handles role bans and playtime but the server ones are separate DEAL. // Yeah the client manager handles role bans and playtime but the server ones are separate DEAL.
_net.RegisterNetMessage<MsgRoleBans>(RxRoleBans); _net.RegisterNetMessage<MsgRoleBans>(RxRoleBans);
_net.RegisterNetMessage<MsgPlayTime>(RxPlayTime); _net.RegisterNetMessage<MsgPlayTime>(RxPlayTime);
_net.RegisterNetMessage<MsgJobWhitelist>(RxJobWhitelist);
_client.RunLevelChanged += ClientOnRunLevelChanged; _client.RunLevelChanged += ClientOnRunLevelChanged;
} }
@@ -79,6 +82,13 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
Updated?.Invoke(); Updated?.Invoke();
} }
private void RxJobWhitelist(MsgJobWhitelist message)
{
_jobWhitelists.Clear();
_jobWhitelists.AddRange(message.Whitelist);
Updated?.Invoke();
}
public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason) public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
{ {
reason = null; reason = null;
@@ -89,6 +99,9 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
return false; return false;
} }
if (!CheckWhitelist(job, out reason))
return false;
var player = _playerManager.LocalSession; var player = _playerManager.LocalSession;
if (player == null) if (player == null)
return true; return true;
@@ -116,6 +129,21 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
return reason == null; return reason == null;
} }
public bool CheckWhitelist(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = default;
if (!_cfg.GetCVar(CCVars.GameRoleWhitelist))
return true;
if (job.Whitelisted && !_jobWhitelists.Contains(job.ID))
{
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-not-whitelisted"));
return false;
}
return true;
}
public TimeSpan FetchOverallPlaytime() public TimeSpan FetchOverallPlaytime()
{ {
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero; return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;

View File

@@ -21,6 +21,7 @@ public static partial class PoolManager
(CCVars.NPCMaxUpdates.Name, "999999"), (CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"), (CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"), (CCVars.GameRoleTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"), (CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"), (CCVars.PreloadGrids.Name, "false"),
(CCVars.ArrivalsShuttles.Name, "false"), (CCVars.ArrivalsShuttles.Name, "false"),

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
#nullable disable
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class RoleWhitelist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "role_whitelists",
columns: table => new
{
player_user_id = table.Column<Guid>(type: "uuid", nullable: false),
role_id = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_role_whitelists", x => new { x.player_user_id, x.role_id });
table.ForeignKey(
name: "FK_role_whitelists_player_player_user_id",
column: x => x.player_user_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "role_whitelists");
}
}
}

View File

@@ -900,6 +900,22 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile_role_loadout", (string)null); b.ToTable("profile_role_loadout", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.Property<Guid>("PlayerUserId")
.HasColumnType("uuid")
.HasColumnName("player_user_id");
b.Property<string>("RoleId")
.HasColumnType("text")
.HasColumnName("role_id");
b.HasKey("PlayerUserId", "RoleId")
.HasName("PK_role_whitelists");
b.ToTable("role_whitelists", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b => modelBuilder.Entity("Content.Server.Database.Round", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1623,6 +1639,19 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile"); b.Navigation("Profile");
}); });
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("JobWhitelists")
.HasForeignKey("PlayerUserId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_role_whitelists_player_player_user_id");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.Round", b => modelBuilder.Entity("Content.Server.Database.Round", b =>
{ {
b.HasOne("Content.Server.Database.Server", "Server") b.HasOne("Content.Server.Database.Server", "Server")
@@ -1822,6 +1851,8 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("AdminWatchlistsLastEdited"); b.Navigation("AdminWatchlistsLastEdited");
b.Navigation("AdminWatchlistsReceived"); b.Navigation("AdminWatchlistsReceived");
b.Navigation("JobWhitelists");
}); });
modelBuilder.Entity("Content.Server.Database.Preference", b => modelBuilder.Entity("Content.Server.Database.Preference", b =>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
#nullable disable
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class RoleWhitelist : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "role_whitelists",
columns: table => new
{
player_user_id = table.Column<Guid>(type: "TEXT", nullable: false),
role_id = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_role_whitelists", x => new { x.player_user_id, x.role_id });
table.ForeignKey(
name: "FK_role_whitelists_player_player_user_id",
column: x => x.player_user_id,
principalTable: "player",
principalColumn: "user_id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "role_whitelists");
}
}
}

View File

@@ -847,6 +847,22 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile_role_loadout", (string)null); b.ToTable("profile_role_loadout", (string)null);
}); });
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.Property<Guid>("PlayerUserId")
.HasColumnType("TEXT")
.HasColumnName("player_user_id");
b.Property<string>("RoleId")
.HasColumnType("TEXT")
.HasColumnName("role_id");
b.HasKey("PlayerUserId", "RoleId")
.HasName("PK_role_whitelists");
b.ToTable("role_whitelists", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b => modelBuilder.Entity("Content.Server.Database.Round", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1548,6 +1564,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile"); b.Navigation("Profile");
}); });
modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
{
b.HasOne("Content.Server.Database.Player", "Player")
.WithMany("JobWhitelists")
.HasForeignKey("PlayerUserId")
.HasPrincipalKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_role_whitelists_player_player_user_id");
b.Navigation("Player");
});
modelBuilder.Entity("Content.Server.Database.Round", b => modelBuilder.Entity("Content.Server.Database.Round", b =>
{ {
b.HasOne("Content.Server.Database.Server", "Server") b.HasOne("Content.Server.Database.Server", "Server")
@@ -1747,6 +1776,8 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("AdminWatchlistsLastEdited"); b.Navigation("AdminWatchlistsLastEdited");
b.Navigation("AdminWatchlistsReceived"); b.Navigation("AdminWatchlistsReceived");
b.Navigation("JobWhitelists");
}); });
modelBuilder.Entity("Content.Server.Database.Preference", b => modelBuilder.Entity("Content.Server.Database.Preference", b =>

View File

@@ -40,6 +40,7 @@ namespace Content.Server.Database
public DbSet<AdminNote> AdminNotes { get; set; } = null!; public DbSet<AdminNote> AdminNotes { get; set; } = null!;
public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!; public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!;
public DbSet<AdminMessage> AdminMessages { get; set; } = null!; public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -314,6 +315,13 @@ namespace Content.Server.Database
.HasForeignKey(ban => ban.LastEditedById) .HasForeignKey(ban => ban.LastEditedById)
.HasPrincipalKey(author => author.UserId) .HasPrincipalKey(author => author.UserId)
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
modelBuilder.Entity<RoleWhitelist>()
.HasOne(w => w.Player)
.WithMany(p => p.JobWhitelists)
.HasForeignKey(w => w.PlayerUserId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
} }
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText) public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -530,6 +538,7 @@ namespace Content.Server.Database
public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!; public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!; public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!; public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
} }
[Table("whitelist")] [Table("whitelist")]
@@ -1099,4 +1108,15 @@ namespace Content.Server.Database
/// </summary> /// </summary>
public bool Dismissed { get; set; } public bool Dismissed { get; set; }
} }
[PrimaryKey(nameof(PlayerUserId), nameof(RoleId))]
public class RoleWhitelist
{
[Required, ForeignKey("Player")]
public Guid PlayerUserId { get; set; }
public Player Player { get; set; } = default!;
[Required]
public string RoleId { get; set; } = default!;
}
} }

View File

@@ -0,0 +1,214 @@
using System.Linq;
using Content.Server.Database;
using Content.Server.Players.JobWhitelist;
using Content.Shared.Administration;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class JobWhitelistAddCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly JobWhitelistManager _jobWhitelist = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
public override string Command => "jobwhitelistadd";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific",
("properAmount", 2),
("currentAmount", args.Length)));
shell.WriteLine(Help);
return;
}
var player = args[0].Trim();
var job = new ProtoId<JobPrototype>(args[1].Trim());
if (!_prototypes.TryIndex(job, out var jobPrototype))
{
shell.WriteError(Loc.GetString("cmd-jobwhitelist-job-does-not-exist", ("job", job.Id)));
shell.WriteLine(Help);
return;
}
var data = await _playerLocator.LookupIdByNameAsync(player);
if (data != null)
{
var guid = data.UserId;
var isWhitelisted = await _db.IsJobWhitelisted(guid, job);
if (isWhitelisted)
{
shell.WriteLine(Loc.GetString("cmd-jobwhitelist-already-whitelisted",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
_jobWhitelist.AddWhitelist(guid, job);
shell.WriteLine(Loc.GetString("cmd-jobwhitelistadd-added",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
_players.Sessions.Select(s => s.Name),
Loc.GetString("cmd-jobwhitelist-hint-player"));
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(
_prototypes.EnumeratePrototypes<JobPrototype>().Select(p => p.ID),
Loc.GetString("cmd-jobwhitelist-hint-job"));
}
return CompletionResult.Empty;
}
}
[AdminCommand(AdminFlags.Ban)]
public sealed class GetJobWhitelistCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IPlayerManager _players = default!;
public override string Command => "jobwhitelistget";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteError("This command needs at least one argument.");
shell.WriteLine(Help);
return;
}
var player = string.Join(' ', args).Trim();
var data = await _playerLocator.LookupIdByNameAsync(player);
if (data != null)
{
var guid = data.UserId;
var whitelists = await _db.GetJobWhitelists(guid);
if (whitelists.Count == 0)
{
shell.WriteLine(Loc.GetString("cmd-jobwhitelistget-whitelisted-none", ("player", player)));
return;
}
shell.WriteLine(Loc.GetString("cmd-jobwhitelistget-whitelisted-for",
("player", player),
("jobs", string.Join(", ", whitelists))));
return;
}
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
_players.Sessions.Select(s => s.Name),
Loc.GetString("cmd-jobwhitelist-hint-player"));
}
return CompletionResult.Empty;
}
}
[AdminCommand(AdminFlags.Ban)]
public sealed class RemoveJobWhitelistCommand : LocalizedCommands
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly JobWhitelistManager _jobWhitelist = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
public override string Command => "jobwhitelistremove";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific",
("properAmount", 2),
("currentAmount", args.Length)));
shell.WriteLine(Help);
return;
}
var player = args[0].Trim();
var job = new ProtoId<JobPrototype>(args[1].Trim());
if (!_prototypes.TryIndex(job, out var jobPrototype))
{
shell.WriteError(Loc.GetString("cmd-jobwhitelist-job-does-not-exist", ("job", job)));
shell.WriteLine(Help);
return;
}
var data = await _playerLocator.LookupIdByNameAsync(player);
if (data != null)
{
var guid = data.UserId;
var isWhitelisted = await _db.IsJobWhitelisted(guid, job);
if (!isWhitelisted)
{
shell.WriteError(Loc.GetString("cmd-jobwhitelistremove-was-not-whitelisted",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
_jobWhitelist.RemoveWhitelist(guid, job);
shell.WriteLine(Loc.GetString("cmd-jobwhitelistremove-removed",
("player", player),
("jobId", job.Id),
("jobName", jobPrototype.LocalizedName)));
return;
}
shell.WriteError(Loc.GetString("cmd-jobwhitelist-player-not-found", ("player", player)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
_players.Sessions.Select(s => s.Name),
Loc.GetString("cmd-jobwhitelist-hint-player"));
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(
_prototypes.EnumeratePrototypes<JobPrototype>().Select(p => p.ID),
Loc.GetString("cmd-jobwhitelist-hint-job"));
}
return CompletionResult.Empty;
}
}

View File

@@ -73,7 +73,9 @@ public sealed class BanManager : IBanManager, IPostInjectInit
public HashSet<string>? GetRoleBans(NetUserId playerUserId) public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{ {
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans) ? roleBans.Select(banDef => banDef.Role).ToHashSet() : null; return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans)
? roleBans.Select(banDef => banDef.Role).ToHashSet()
: null;
} }
private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null) private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null)
@@ -263,13 +265,13 @@ public sealed class BanManager : IBanManager, IPostInjectInit
return $"Pardoned ban with id {banId}"; return $"Pardoned ban with id {banId}";
} }
public HashSet<string>? GetJobBans(NetUserId playerUserId) public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
{ {
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans)) if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
return null; return null;
return roleBans return roleBans
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal)) .Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
.Select(ban => ban.Role[JobPrefix.Length..]) .Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
.ToHashSet(); .ToHashSet();
} }
#endregion #endregion

View File

@@ -2,8 +2,10 @@ using System.Collections.Immutable;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Managers; namespace Content.Server.Administration.Managers;
@@ -24,7 +26,7 @@ public interface IBanManager
/// <param name="reason">Reason for the ban</param> /// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason); public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason);
public HashSet<string>? GetRoleBans(NetUserId playerUserId); public HashSet<string>? GetRoleBans(NetUserId playerUserId);
public HashSet<string>? GetJobBans(NetUserId playerUserId); public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
/// <summary> /// <summary>
/// Creates a job ban for the specified target, username or GUID /// Creates a job ban for the specified target, username or GUID

View File

@@ -14,9 +14,11 @@ using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Database namespace Content.Server.Database
@@ -1579,6 +1581,65 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
#endregion #endregion
#region Job Whitelists
public async Task<bool> AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
{
await using var db = await GetDb();
var exists = await db.DbContext.RoleWhitelists
.Where(w => w.PlayerUserId == player)
.Where(w => w.RoleId == job.Id)
.AnyAsync();
if (exists)
return false;
var whitelist = new RoleWhitelist
{
PlayerUserId = player,
RoleId = job
};
db.DbContext.RoleWhitelists.Add(whitelist);
await db.DbContext.SaveChangesAsync();
return true;
}
public async Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel)
{
await using var db = await GetDb(cancel);
return await db.DbContext.RoleWhitelists
.Where(w => w.PlayerUserId == player)
.Select(w => w.RoleId)
.ToListAsync(cancellationToken: cancel);
}
public async Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
{
await using var db = await GetDb();
return await db.DbContext.RoleWhitelists
.Where(w => w.PlayerUserId == player)
.Where(w => w.RoleId == job.Id)
.AnyAsync();
}
public async Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
{
await using var db = await GetDb();
var entry = await db.DbContext.RoleWhitelists
.Where(w => w.PlayerUserId == player)
.Where(w => w.RoleId == job.Id)
.SingleOrDefaultAsync();
if (entry == null)
return false;
db.DbContext.RoleWhitelists.Remove(entry);
await db.DbContext.SaveChangesAsync();
return true;
}
#endregion
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc. // SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
// Normalize DateTimes here so they're always Utc. Thanks. // Normalize DateTimes here so they're always Utc. Thanks.
protected abstract DateTime NormalizeDatabaseTime(DateTime time); protected abstract DateTime NormalizeDatabaseTime(DateTime time);

View File

@@ -9,6 +9,7 @@ using Content.Shared.Administration.Logs;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -17,6 +18,7 @@ using Prometheus;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using LogLevel = Robust.Shared.Log.LogLevel; using LogLevel = Robust.Shared.Log.LogLevel;
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel; using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
@@ -290,6 +292,18 @@ namespace Content.Server.Database
Task MarkMessageAsSeen(int id, bool dismissedToo); Task MarkMessageAsSeen(int id, bool dismissedToo);
#endregion #endregion
#region Job Whitelists
Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job);
Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default);
Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job);
Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job);
#endregion
} }
public sealed class ServerDbManager : IServerDbManager public sealed class ServerDbManager : IServerDbManager
@@ -869,6 +883,30 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo)); return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo));
} }
public Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddJobWhitelist(player, job));
}
public Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetJobWhitelists(player, cancel));
}
public Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.IsJobWhitelisted(player, job));
}
public Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
}
// Wrapper functions to run DB commands from the thread pool. // Wrapper functions to run DB commands from the thread pool.
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread. // This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
// For SQLite, this will also enable read parallelization (within limits). // For SQLite, this will also enable read parallelization (within limits).

View File

@@ -1,8 +1,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Robust.Server.Player;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -19,11 +17,12 @@ namespace Content.Server.Database;
/// </remarks> /// </remarks>
public sealed class UserDbDataManager : IPostInjectInit public sealed class UserDbDataManager : IPostInjectInit
{ {
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
private readonly Dictionary<NetUserId, UserData> _users = new(); private readonly Dictionary<NetUserId, UserData> _users = new();
private readonly List<OnLoadPlayer> _onLoadPlayer = [];
private readonly List<OnFinishLoad> _onFinishLoad = [];
private readonly List<OnPlayerDisconnect> _onPlayerDisconnect = [];
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
@@ -51,8 +50,10 @@ public sealed class UserDbDataManager : IPostInjectInit
data.Cancel.Cancel(); data.Cancel.Cancel();
data.Cancel.Dispose(); data.Cancel.Dispose();
_prefs.OnClientDisconnected(session); foreach (var onDisconnect in _onPlayerDisconnect)
_playTimeTracking.ClientDisconnected(session); {
onDisconnect(session);
}
} }
private async Task Load(ICommonSession session, CancellationToken cancel) private async Task Load(ICommonSession session, CancellationToken cancel)
@@ -62,12 +63,20 @@ public sealed class UserDbDataManager : IPostInjectInit
// As such, this task must NOT throw a non-cancellation error! // As such, this task must NOT throw a non-cancellation error!
try try
{ {
await Task.WhenAll( var tasks = new List<Task>();
_prefs.LoadData(session, cancel), foreach (var action in _onLoadPlayer)
_playTimeTracking.LoadData(session, cancel)); {
tasks.Add(action(session, cancel));
}
await Task.WhenAll(tasks);
cancel.ThrowIfCancellationRequested(); cancel.ThrowIfCancellationRequested();
_prefs.FinishLoad(session);
foreach (var action in _onFinishLoad)
{
action(session);
}
_sawmill.Verbose($"Load complete for user {session}"); _sawmill.Verbose($"Load complete for user {session}");
} }
@@ -118,10 +127,31 @@ public sealed class UserDbDataManager : IPostInjectInit
return _users[session.UserId].Task; return _users[session.UserId].Task;
} }
public void AddOnLoadPlayer(OnLoadPlayer action)
{
_onLoadPlayer.Add(action);
}
public void AddOnFinishLoad(OnFinishLoad action)
{
_onFinishLoad.Add(action);
}
public void AddOnPlayerDisconnect(OnPlayerDisconnect action)
{
_onPlayerDisconnect.Add(action);
}
void IPostInjectInit.PostInject() void IPostInjectInit.PostInject()
{ {
_sawmill = _logManager.GetSawmill("userdb"); _sawmill = _logManager.GetSawmill("userdb");
} }
private sealed record UserData(CancellationTokenSource Cancel, Task Task); private sealed record UserData(CancellationTokenSource Cancel, Task Task);
public delegate Task OnLoadPlayer(ICommonSession player, CancellationToken cancel);
public delegate void OnFinishLoad(ICommonSession player);
public delegate void OnPlayerDisconnect(ICommonSession player);
} }

View File

@@ -14,6 +14,7 @@ using Content.Server.Info;
using Content.Server.IoC; using Content.Server.IoC;
using Content.Server.Maps; using Content.Server.Maps;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking; using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Server.ServerInfo; using Content.Server.ServerInfo;
@@ -107,6 +108,7 @@ namespace Content.Server.Entry
_voteManager.Initialize(); _voteManager.Initialize();
_updateManager.Initialize(); _updateManager.Initialize();
_playTimeTracking.Initialize(); _playTimeTracking.Initialize();
IoCManager.Resolve<JobWhitelistManager>().Initialize();
} }
} }

View File

@@ -0,0 +1,8 @@
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Events;
[ByRefEvent]
public readonly record struct GetDisallowedJobsEvent(ICommonSession Player, HashSet<ProtoId<JobPrototype>> Jobs);

View File

@@ -0,0 +1,13 @@
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Events;
[ByRefEvent]
public struct IsJobAllowedEvent(ICommonSession player, ProtoId<JobPrototype> jobId, bool cancelled = false)
{
public readonly ICommonSession Player = player;
public readonly ProtoId<JobPrototype> JobId = jobId;
public bool Cancelled = cancelled;
}

View File

@@ -2,11 +2,11 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost; using Content.Server.Ghost;
using Content.Server.Spawners.Components; using Content.Server.Spawners.Components;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Players; using Content.Shared.Players;
@@ -137,8 +137,14 @@ namespace Content.Server.GameTicking
if (jobBans == null || jobId != null && jobBans.Contains(jobId)) if (jobBans == null || jobId != null && jobBans.Contains(jobId))
return; return;
if (jobId != null && !_playTimeTrackings.IsAllowed(player, jobId)) if (jobId != null)
{
var ev = new IsJobAllowedEvent(player, new ProtoId<JobPrototype>(jobId));
RaiseLocalEvent(ref ev);
if (ev.Cancelled)
return; return;
}
SpawnPlayer(player, character, station, jobId, lateJoin, silent); SpawnPlayer(player, character, station, jobId, lateJoin, silent);
} }
@@ -181,10 +187,9 @@ namespace Content.Server.GameTicking
} }
// Figure out job restrictions // Figure out job restrictions
var restrictedRoles = new HashSet<string>(); var restrictedRoles = new HashSet<ProtoId<JobPrototype>>();
var ev = new GetDisallowedJobsEvent(player, restrictedRoles);
var getDisallowed = _playTimeTrackings.GetDisallowedJobs(player); RaiseLocalEvent(ref ev);
restrictedRoles.UnionWith(getDisallowed);
var jobBans = _banManager.GetJobBans(player.UserId); var jobBans = _banManager.GetJobBans(player.UserId);
if (jobBans != null) if (jobBans != null)

View File

@@ -19,7 +19,6 @@ using Content.Shared.Roles;
using Robust.Server; using Robust.Server;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.GameStates; using Robust.Server.GameStates;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Console; using Robust.Shared.Console;

View File

@@ -13,6 +13,7 @@ using Content.Server.Info;
using Content.Server.Maps; using Content.Server.Maps;
using Content.Server.MoMMI; using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking; using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers; using Content.Server.Preferences.Managers;
using Content.Server.ServerInfo; using Content.Server.ServerInfo;
@@ -61,6 +62,7 @@ namespace Content.Server.IoC
IoCManager.Register<ServerDbEntryManager>(); IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>(); IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>(); IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
} }
} }
} }

View File

@@ -0,0 +1,114 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.CCVar;
using Content.Shared.Players.JobWhitelist;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Serilog;
namespace Content.Server.Players.JobWhitelist;
public sealed class JobWhitelistManager : IPostInjectInit
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
private readonly Dictionary<NetUserId, HashSet<string>> _whitelists = new();
public void Initialize()
{
_net.RegisterNetMessage<MsgJobWhitelist>();
}
private async Task LoadData(ICommonSession session, CancellationToken cancel)
{
var whitelists = await _db.GetJobWhitelists(session.UserId, cancel);
cancel.ThrowIfCancellationRequested();
_whitelists[session.UserId] = whitelists.ToHashSet();
}
private void FinishLoad(ICommonSession session)
{
SendJobWhitelist(session);
}
private void ClientDisconnected(ICommonSession session)
{
_whitelists.Remove(session.UserId);
}
public async void AddWhitelist(NetUserId player, ProtoId<JobPrototype> job)
{
if (_whitelists.TryGetValue(player, out var whitelists))
whitelists.Add(job);
await _db.AddJobWhitelist(player, job);
if (_player.TryGetSessionById(player, out var session))
SendJobWhitelist(session);
}
public bool IsAllowed(ICommonSession session, ProtoId<JobPrototype> job)
{
if (!_config.GetCVar(CCVars.GameRoleWhitelist))
return true;
if (!_prototypes.TryIndex(job, out var jobPrototype) ||
!jobPrototype.Whitelisted)
{
return true;
}
return IsWhitelisted(session.UserId, job);
}
public bool IsWhitelisted(NetUserId player, ProtoId<JobPrototype> job)
{
if (!_whitelists.TryGetValue(player, out var whitelists))
{
Log.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}",
player,
job,
Environment.StackTrace);
return false;
}
return whitelists.Contains(job);
}
public async void RemoveWhitelist(NetUserId player, ProtoId<JobPrototype> job)
{
_whitelists.GetValueOrDefault(player)?.Remove(job);
await _db.RemoveJobWhitelist(player, job);
if (_player.TryGetSessionById(new NetUserId(player), out var session))
SendJobWhitelist(session);
}
public void SendJobWhitelist(ICommonSession player)
{
var msg = new MsgJobWhitelist
{
Whitelist = _whitelists.GetValueOrDefault(player.UserId) ?? new HashSet<string>()
};
_net.ServerSendMessage(msg, player.Channel);
}
void IPostInjectInit.PostInject()
{
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnFinishLoad(FinishLoad);
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
}
}

View File

@@ -0,0 +1,83 @@
using System.Collections.Immutable;
using Content.Server.GameTicking.Events;
using Content.Server.Station.Events;
using Content.Shared.CCVar;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Players.JobWhitelist;
public sealed class JobWhitelistSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly JobWhitelistManager _manager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
private ImmutableArray<ProtoId<JobPrototype>> _whitelistedJobs = [];
public override void Initialize()
{
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
CacheJobs();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
{
if (ev.WasModified<JobPrototype>())
CacheJobs();
}
private void OnStationJobsGetCandidates(ref StationJobsGetCandidatesEvent ev)
{
if (!_config.GetCVar(CCVars.GameRoleWhitelist))
return;
for (var i = ev.Jobs.Count - 1; i >= 0; i--)
{
var jobId = ev.Jobs[i];
if (_player.TryGetSessionById(ev.Player, out var player) &&
!_manager.IsAllowed(player, jobId))
{
ev.Jobs.RemoveSwap(i);
}
}
}
private void OnIsJobAllowed(ref IsJobAllowedEvent ev)
{
if (!_manager.IsAllowed(ev.Player, ev.JobId))
ev.Cancelled = true;
}
private void OnGetDisallowedJobs(ref GetDisallowedJobsEvent ev)
{
if (!_config.GetCVar(CCVars.GameRoleWhitelist))
return;
foreach (var job in _whitelistedJobs)
{
if (!_manager.IsAllowed(ev.Player, job))
ev.Jobs.Add(job);
}
}
private void CacheJobs()
{
var builder = ImmutableArray.CreateBuilder<ProtoId<JobPrototype>>();
foreach (var job in _prototypes.EnumeratePrototypes<JobPrototype>())
{
if (job.Whitelisted)
builder.Add(job.ID);
}
_whitelistedJobs = builder.ToImmutable();
}
}

View File

@@ -54,7 +54,7 @@ public delegate void CalcPlayTimeTrackersCallback(ICommonSession player, HashSet
/// Operations like refreshing and sending play time info to clients are deferred until the next frame (note: not tick). /// Operations like refreshing and sending play time info to clients are deferred until the next frame (note: not tick).
/// </para> /// </para>
/// </remarks> /// </remarks>
public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager, IPostInjectInit
{ {
[Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IServerNetManager _net = default!; [Dependency] private readonly IServerNetManager _net = default!;
@@ -62,6 +62,7 @@ public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ITaskManager _task = default!; [Dependency] private readonly ITaskManager _task = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!; [Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
@@ -445,4 +446,10 @@ public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
/// </summary> /// </summary>
public readonly HashSet<string> DbTrackersDirty = new(); public readonly HashSet<string> DbTrackersDirty = new();
} }
void IPostInjectInit.PostInject()
{
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
}
} }

View File

@@ -4,7 +4,9 @@ using Content.Server.Administration.Managers;
using Content.Server.Afk; using Content.Server.Afk;
using Content.Server.Afk.Events; using Content.Server.Afk.Events;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.Station.Events;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Mobs; using Content.Shared.Mobs;
@@ -12,7 +14,6 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Players; using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Server.GameObjects;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -50,6 +51,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
SubscribeLocalEvent<UnAFKEvent>(OnUnAFK); SubscribeLocalEvent<UnAFKEvent>(OnUnAFK);
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged); SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby); SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
_adminManager.OnPermsChanged += AdminPermsChanged; _adminManager.OnPermsChanged += AdminPermsChanged;
} }
@@ -174,6 +178,22 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
_tracking.QueueSendTimers(ev.PlayerSession); _tracking.QueueSendTimers(ev.PlayerSession);
} }
private void OnStationJobsGetCandidates(ref StationJobsGetCandidatesEvent ev)
{
RemoveDisallowedJobs(ev.Player, ev.Jobs);
}
private void OnIsJobAllowed(ref IsJobAllowedEvent ev)
{
if (!IsAllowed(ev.Player, ev.JobId))
ev.Cancelled = true;
}
private void OnGetDisallowedJobs(ref GetDisallowedJobsEvent ev)
{
ev.Jobs.UnionWith(GetDisallowedJobs(ev.Player));
}
public bool IsAllowed(ICommonSession player, string role) public bool IsAllowed(ICommonSession player, string role)
{ {
if (!_prototypes.TryIndex<JobPrototype>(role, out var job) || if (!_prototypes.TryIndex<JobPrototype>(role, out var job) ||
@@ -190,9 +210,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes); return JobRequirements.TryRequirementsMet(job, playTimes, out _, EntityManager, _prototypes);
} }
public HashSet<string> GetDisallowedJobs(ICommonSession player) public HashSet<ProtoId<JobPrototype>> GetDisallowedJobs(ICommonSession player)
{ {
var roles = new HashSet<string>(); var roles = new HashSet<ProtoId<JobPrototype>>();
if (!_cfg.GetCVar(CCVars.GameRoleTimers)) if (!_cfg.GetCVar(CCVars.GameRoleTimers))
return roles; return roles;
@@ -222,7 +242,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
return roles; return roles;
} }
public void RemoveDisallowedJobs(NetUserId userId, ref List<string> jobs) public void RemoveDisallowedJobs(NetUserId userId, List<ProtoId<JobPrototype>> jobs)
{ {
if (!_cfg.GetCVar(CCVars.GameRoleTimers)) if (!_cfg.GetCVar(CCVars.GameRoleTimers))
return; return;
@@ -239,7 +259,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
{ {
var job = jobs[i]; var job = jobs[i];
if (!_prototypes.TryIndex<JobPrototype>(job, out var jobber) || if (!_prototypes.TryIndex(job, out var jobber) ||
jobber.Requirements == null || jobber.Requirements == null ||
jobber.Requirements.Count == 0) jobber.Requirements.Count == 0)
continue; continue;

View File

@@ -3,26 +3,21 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Database; using Content.Server.Database;
using Content.Server.Humanoid;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Preferences.Managers namespace Content.Server.Preferences.Managers
{ {
/// <summary> /// <summary>
/// Sends <see cref="MsgPreferencesAndSettings"/> before the client joins the lobby. /// Sends <see cref="MsgPreferencesAndSettings"/> before the client joins the lobby.
/// Receives <see cref="MsgSelectCharacter"/> and <see cref="MsgUpdateCharacter"/> at any time. /// Receives <see cref="MsgSelectCharacter"/> and <see cref="MsgUpdateCharacter"/> at any time.
/// </summary> /// </summary>
public sealed class ServerPreferencesManager : IServerPreferencesManager public sealed class ServerPreferencesManager : IServerPreferencesManager, IPostInjectInit
{ {
[Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -30,6 +25,7 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IDependencyCollection _dependencies = default!; [Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly ILogManager _log = default!; [Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
// Cache player prefs on the server so we don't need as much async hell related to them. // Cache player prefs on the server so we don't need as much async hell related to them.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs = private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
@@ -326,5 +322,12 @@ namespace Content.Server.Preferences.Managers
public bool PrefsLoaded; public bool PrefsLoaded;
public PlayerPreferences? Prefs; public PlayerPreferences? Prefs;
} }
void IPostInjectInit.PostInject()
{
_userDb.AddOnLoadPlayer(LoadData);
_userDb.AddOnFinishLoad(FinishLoad);
_userDb.AddOnPlayerDisconnect(OnClientDisconnected);
}
} }
} }

View File

@@ -0,0 +1,8 @@
using Content.Shared.Roles;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Station.Events;
[ByRefEvent]
public readonly record struct StationJobsGetCandidatesEvent(NetUserId Player, List<ProtoId<JobPrototype>> Jobs);

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Players.PlayTimeTracking; using Content.Server.Players.PlayTimeTracking;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -342,8 +343,9 @@ public sealed partial class StationJobsSystem
foreach (var (player, profile) in profiles) foreach (var (player, profile) in profiles)
{ {
var roleBans = _banManager.GetJobBans(player); var roleBans = _banManager.GetJobBans(player);
var profileJobs = profile.JobPriorities.Keys.ToList(); var profileJobs = profile.JobPriorities.Keys.Select(k => new ProtoId<JobPrototype>(k)).ToList();
_playTime.RemoveDisallowedJobs(player, ref profileJobs); var ev = new StationJobsGetCandidatesEvent(player, profileJobs);
RaiseLocalEvent(ref ev);
List<string>? availableJobs = null; List<string>? availableJobs = null;
@@ -354,7 +356,7 @@ public sealed partial class StationJobsSystem
if (!(priority == selectedPriority || selectedPriority is null)) if (!(priority == selectedPriority || selectedPriority is null))
continue; continue;
if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job)) if (!_prototypeManager.TryIndex(jobId, out var job))
continue; continue;
if (weight is not null && job.Weight != weight.Value) if (weight is not null && job.Weight != weight.Value)

View File

@@ -428,7 +428,7 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="pickOverflows">Whether or not to pick from the overflow list.</param> /// <param name="pickOverflows">Whether or not to pick from the overflow list.</param>
/// <param name="disallowedJobs">A set of disallowed jobs, if any.</param> /// <param name="disallowedJobs">A set of disallowed jobs, if any.</param>
/// <returns>The selected job, if any.</returns> /// <returns>The selected job, if any.</returns>
public string? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary<string, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet<string>? disallowedJobs = null) public string? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary<string, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet<ProtoId<JobPrototype>>? disallowedJobs = null)
{ {
if (station == EntityUid.Invalid) if (station == EntityUid.Invalid)
return null; return null;

View File

@@ -225,6 +225,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> public static readonly CVarDef<bool>
GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED); GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// If roles should be restricted based on whether or not they are whitelisted.
/// </summary>
public static readonly CVarDef<bool>
GameRoleWhitelist = CVarDef.Create("game.role_whitelist", true, CVar.SERVER | CVar.REPLICATED);
/// <summary> /// <summary>
/// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect. /// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect.
/// </summary> /// </summary>

View File

@@ -0,0 +1,33 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared.Players.JobWhitelist;
public sealed class MsgJobWhitelist : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.EntityEvent;
public HashSet<string> Whitelist = new();
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
var count = buffer.ReadVariableInt32();
Whitelist.EnsureCapacity(count);
for (var i = 0; i < count; i++)
{
Whitelist.Add(buffer.ReadString());
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.WriteVariableInt32(Whitelist.Count);
foreach (var ban in Whitelist)
{
buffer.Write(ban);
}
}
}

View File

@@ -1,10 +1,8 @@
using Content.Shared.Access; using Content.Shared.Access;
using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Content.Shared.StatusIcon; using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Roles namespace Content.Shared.Roles
{ {
@@ -116,6 +114,9 @@ namespace Content.Shared.Roles
[DataField("extendedAccessGroups")] [DataField("extendedAccessGroups")]
public IReadOnlyCollection<ProtoId<AccessGroupPrototype>> ExtendedAccessGroups { get; private set; } = Array.Empty<ProtoId<AccessGroupPrototype>>(); public IReadOnlyCollection<ProtoId<AccessGroupPrototype>> ExtendedAccessGroups { get; private set; } = Array.Empty<ProtoId<AccessGroupPrototype>>();
[DataField]
public bool Whitelisted;
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,20 @@
cmd-jobwhitelist-job-does-not-exist = Job {$job} does not exist.
cmd-jobwhitelist-player-not-found = Player {$player} not found.
cmd-jobwhitelist-hint-player = [player]
cmd-jobwhitelist-hint-job = [job]
cmd-jobwhitelistadd-desc = Lets a player play a whitelisted job.
cmd-jobwhitelistadd-help = Usage: jobwhitelistadd <username> <job>
cmd-jobwhitelistadd-already-whitelisted = {$player} is already whitelisted to play as {$jobId} .({$jobName}).
cmd-jobwhitelistadd-added = Added {$player} to the {$jobId} ({$jobName}) whitelist.
cmd-jobwhitelistget-desc = Gets all the jobs that a player has been whitelisted for.
cmd-jobwhitelistget-help = Usage: jobwhitelistadd <username>
cmd-jobwhitelistget-whitelisted-none = Player {$player} is not whitelisted for any jobs.
cmd-jobwhitelistget-whitelisted-for = "Player {$player} is whitelisted for:
{$jobs}"
cmd-jobwhitelistremove-desc = Removes a player's ability to play a whitelisted job.
cmd-jobwhitelistremove-help = Usage: jobwhitelistadd <username> <job>
cmd-jobwhitelistremove-was-not-whitelisted = {$player} was not whitelisted to play as {$jobId} ({$jobName}).
cmd-jobwhitelistremove-removed = Removed {$player} from the whitelist for {$jobId} ({$jobName}).

View File

@@ -0,0 +1 @@
role-not-whitelisted = You are not whitelisted to play this role.