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:
@@ -22,7 +22,6 @@ using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Lobby;
|
||||
@@ -70,12 +69,9 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
_profileEditor?.RefreshFlavorText();
|
||||
});
|
||||
|
||||
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, args =>
|
||||
{
|
||||
_profileEditor?.RefreshAntags();
|
||||
_profileEditor?.RefreshJobs();
|
||||
_profileEditor?.RefreshLoadouts();
|
||||
});
|
||||
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
|
||||
|
||||
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
|
||||
}
|
||||
|
||||
private LobbyCharacterPreviewPanel? GetLobbyPreview()
|
||||
@@ -193,6 +189,13 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
PreviewPanel.SetSummaryText(humanoid.Summary);
|
||||
}
|
||||
|
||||
private void RefreshProfileEditor()
|
||||
{
|
||||
_profileEditor?.RefreshAntags();
|
||||
_profileEditor?.RefreshJobs();
|
||||
_profileEditor?.RefreshLoadouts();
|
||||
}
|
||||
|
||||
private void SaveProfile()
|
||||
{
|
||||
DebugTools.Assert(EditedProfile != null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Players.JobWhitelist;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Client;
|
||||
@@ -24,6 +25,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
|
||||
private readonly Dictionary<string, TimeSpan> _roles = new();
|
||||
private readonly List<string> _roleBans = new();
|
||||
private readonly List<string> _jobWhitelists = new();
|
||||
|
||||
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.
|
||||
_net.RegisterNetMessage<MsgRoleBans>(RxRoleBans);
|
||||
_net.RegisterNetMessage<MsgPlayTime>(RxPlayTime);
|
||||
_net.RegisterNetMessage<MsgJobWhitelist>(RxJobWhitelist);
|
||||
|
||||
_client.RunLevelChanged += ClientOnRunLevelChanged;
|
||||
}
|
||||
@@ -79,6 +82,13 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
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)
|
||||
{
|
||||
reason = null;
|
||||
@@ -89,6 +99,9 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CheckWhitelist(job, out reason))
|
||||
return false;
|
||||
|
||||
var player = _playerManager.LocalSession;
|
||||
if (player == null)
|
||||
return true;
|
||||
@@ -116,6 +129,21 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
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()
|
||||
{
|
||||
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
|
||||
|
||||
@@ -21,6 +21,7 @@ public static partial class PoolManager
|
||||
(CCVars.NPCMaxUpdates.Name, "999999"),
|
||||
(CVars.ThreadParallelCount.Name, "1"),
|
||||
(CCVars.GameRoleTimers.Name, "false"),
|
||||
(CCVars.GameRoleWhitelist.Name, "false"),
|
||||
(CCVars.GridFill.Name, "false"),
|
||||
(CCVars.PreloadGrids.Name, "false"),
|
||||
(CCVars.ArrivalsShuttles.Name, "false"),
|
||||
|
||||
1913
Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs
generated
Normal file
1913
Content.Server.Database/Migrations/Postgres/20240531011555_RoleWhitelist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -900,6 +900,22 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1623,6 +1639,19 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Server", "Server")
|
||||
@@ -1822,6 +1851,8 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.Navigation("AdminWatchlistsLastEdited");
|
||||
|
||||
b.Navigation("AdminWatchlistsReceived");
|
||||
|
||||
b.Navigation("JobWhitelists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Preference", b =>
|
||||
|
||||
1838
Content.Server.Database/Migrations/Sqlite/20240531011549_RoleWhitelist.Designer.cs
generated
Normal file
1838
Content.Server.Database/Migrations/Sqlite/20240531011549_RoleWhitelist.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,6 +847,22 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1548,6 +1564,19 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Server", "Server")
|
||||
@@ -1747,6 +1776,8 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.Navigation("AdminWatchlistsLastEdited");
|
||||
|
||||
b.Navigation("AdminWatchlistsReceived");
|
||||
|
||||
b.Navigation("JobWhitelists");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Preference", b =>
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace Content.Server.Database
|
||||
public DbSet<AdminNote> AdminNotes { get; set; } = null!;
|
||||
public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!;
|
||||
public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
|
||||
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -314,6 +315,13 @@ namespace Content.Server.Database
|
||||
.HasForeignKey(ban => ban.LastEditedById)
|
||||
.HasPrincipalKey(author => author.UserId)
|
||||
.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)
|
||||
@@ -530,6 +538,7 @@ namespace Content.Server.Database
|
||||
public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
|
||||
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
|
||||
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
|
||||
public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
|
||||
}
|
||||
|
||||
[Table("whitelist")]
|
||||
@@ -1099,4 +1108,15 @@ namespace Content.Server.Database
|
||||
/// </summary>
|
||||
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!;
|
||||
}
|
||||
}
|
||||
|
||||
214
Content.Server/Administration/Commands/JobWhitelistCommands.cs
Normal file
214
Content.Server/Administration/Commands/JobWhitelistCommands.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,9 @@ public sealed class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
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)
|
||||
@@ -263,13 +265,13 @@ public sealed class BanManager : IBanManager, IPostInjectInit
|
||||
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))
|
||||
return null;
|
||||
return roleBans
|
||||
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
|
||||
.Select(ban => ban.Role[JobPrefix.Length..])
|
||||
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
|
||||
.ToHashSet();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -2,8 +2,10 @@ using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
@@ -24,7 +26,7 @@ public interface IBanManager
|
||||
/// <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 HashSet<string>? GetRoleBans(NetUserId playerUserId);
|
||||
public HashSet<string>? GetJobBans(NetUserId playerUserId);
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a job ban for the specified target, username or GUID
|
||||
|
||||
@@ -14,9 +14,11 @@ using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Database
|
||||
@@ -1579,6 +1581,65 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
#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.
|
||||
// Normalize DateTimes here so they're always Utc. Thanks.
|
||||
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -17,6 +18,7 @@ using Prometheus;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using LogLevel = Robust.Shared.Log.LogLevel;
|
||||
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
@@ -290,6 +292,18 @@ namespace Content.Server.Database
|
||||
Task MarkMessageAsSeen(int id, bool dismissedToo);
|
||||
|
||||
#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
|
||||
@@ -869,6 +883,30 @@ namespace Content.Server.Database
|
||||
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.
|
||||
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
|
||||
// For SQLite, this will also enable read parallelization (within limits).
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -19,11 +17,12 @@ namespace Content.Server.Database;
|
||||
/// </remarks>
|
||||
public sealed class UserDbDataManager : IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
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!;
|
||||
|
||||
@@ -51,8 +50,10 @@ public sealed class UserDbDataManager : IPostInjectInit
|
||||
data.Cancel.Cancel();
|
||||
data.Cancel.Dispose();
|
||||
|
||||
_prefs.OnClientDisconnected(session);
|
||||
_playTimeTracking.ClientDisconnected(session);
|
||||
foreach (var onDisconnect in _onPlayerDisconnect)
|
||||
{
|
||||
onDisconnect(session);
|
||||
}
|
||||
}
|
||||
|
||||
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!
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_prefs.LoadData(session, cancel),
|
||||
_playTimeTracking.LoadData(session, cancel));
|
||||
var tasks = new List<Task>();
|
||||
foreach (var action in _onLoadPlayer)
|
||||
{
|
||||
tasks.Add(action(session, cancel));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
_prefs.FinishLoad(session);
|
||||
|
||||
foreach (var action in _onFinishLoad)
|
||||
{
|
||||
action(session);
|
||||
}
|
||||
|
||||
_sawmill.Verbose($"Load complete for user {session}");
|
||||
}
|
||||
@@ -118,10 +127,31 @@ public sealed class UserDbDataManager : IPostInjectInit
|
||||
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()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("userdb");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Server.Info;
|
||||
using Content.Server.IoC;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Players.JobWhitelist;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerInfo;
|
||||
@@ -107,6 +108,7 @@ namespace Content.Server.Entry
|
||||
_voteManager.Initialize();
|
||||
_updateManager.Initialize();
|
||||
_playTimeTracking.Initialize();
|
||||
IoCManager.Resolve<JobWhitelistManager>().Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
13
Content.Server/GameTicking/Events/IsJobAllowedEvent.cs
Normal file
13
Content.Server/GameTicking/Events/IsJobAllowedEvent.cs
Normal 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;
|
||||
}
|
||||
@@ -2,11 +2,11 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Players;
|
||||
@@ -137,8 +137,14 @@ namespace Content.Server.GameTicking
|
||||
if (jobBans == null || jobId != null && jobBans.Contains(jobId))
|
||||
return;
|
||||
|
||||
if (jobId != null && !_playTimeTrackings.IsAllowed(player, jobId))
|
||||
return;
|
||||
if (jobId != null)
|
||||
{
|
||||
var ev = new IsJobAllowedEvent(player, new ProtoId<JobPrototype>(jobId));
|
||||
RaiseLocalEvent(ref ev);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnPlayer(player, character, station, jobId, lateJoin, silent);
|
||||
}
|
||||
|
||||
@@ -181,10 +187,9 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
|
||||
// Figure out job restrictions
|
||||
var restrictedRoles = new HashSet<string>();
|
||||
|
||||
var getDisallowed = _playTimeTrackings.GetDisallowedJobs(player);
|
||||
restrictedRoles.UnionWith(getDisallowed);
|
||||
var restrictedRoles = new HashSet<ProtoId<JobPrototype>>();
|
||||
var ev = new GetDisallowedJobsEvent(player, restrictedRoles);
|
||||
RaiseLocalEvent(ref ev);
|
||||
|
||||
var jobBans = _banManager.GetJobBans(player.UserId);
|
||||
if (jobBans != null)
|
||||
|
||||
@@ -19,7 +19,6 @@ using Content.Shared.Roles;
|
||||
using Robust.Server;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Server.Info;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.MoMMI;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Players.JobWhitelist;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerInfo;
|
||||
@@ -61,6 +62,7 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<ServerDbEntryManager>();
|
||||
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
|
||||
IoCManager.Register<ServerApi>();
|
||||
IoCManager.Register<JobWhitelistManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
Content.Server/Players/JobWhitelist/JobWhitelistManager.cs
Normal file
114
Content.Server/Players/JobWhitelist/JobWhitelistManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
83
Content.Server/Players/JobWhitelist/JobWhitelistSystem.cs
Normal file
83
Content.Server/Players/JobWhitelist/JobWhitelistSystem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
|
||||
public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _db = 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 ITaskManager _task = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
[Dependency] private readonly UserDbDataManager _userDb = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
@@ -445,4 +446,10 @@ public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
|
||||
/// </summary>
|
||||
public readonly HashSet<string> DbTrackersDirty = new();
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_userDb.AddOnLoadPlayer(LoadData);
|
||||
_userDb.AddOnPlayerDisconnect(ClientDisconnected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ using Content.Server.Administration.Managers;
|
||||
using Content.Server.Afk;
|
||||
using Content.Server.Afk.Events;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Station.Events;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mobs;
|
||||
@@ -12,7 +14,6 @@ using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
@@ -50,6 +51,9 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
SubscribeLocalEvent<UnAFKEvent>(OnUnAFK);
|
||||
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
|
||||
SubscribeLocalEvent<StationJobsGetCandidatesEvent>(OnStationJobsGetCandidates);
|
||||
SubscribeLocalEvent<IsJobAllowedEvent>(OnIsJobAllowed);
|
||||
SubscribeLocalEvent<GetDisallowedJobsEvent>(OnGetDisallowedJobs);
|
||||
_adminManager.OnPermsChanged += AdminPermsChanged;
|
||||
}
|
||||
|
||||
@@ -174,6 +178,22 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
_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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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))
|
||||
return roles;
|
||||
|
||||
@@ -222,7 +242,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
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))
|
||||
return;
|
||||
@@ -239,7 +259,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
{
|
||||
var job = jobs[i];
|
||||
|
||||
if (!_prototypes.TryIndex<JobPrototype>(job, out var jobber) ||
|
||||
if (!_prototypes.TryIndex(job, out var jobber) ||
|
||||
jobber.Requirements == null ||
|
||||
jobber.Requirements.Count == 0)
|
||||
continue;
|
||||
|
||||
@@ -3,26 +3,21 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
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 Robust.Shared.Utility;
|
||||
|
||||
|
||||
namespace Content.Server.Preferences.Managers
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends <see cref="MsgPreferencesAndSettings"/> before the client joins the lobby.
|
||||
/// Receives <see cref="MsgSelectCharacter"/> and <see cref="MsgUpdateCharacter"/> at any time.
|
||||
/// </summary>
|
||||
public sealed class ServerPreferencesManager : IServerPreferencesManager
|
||||
public sealed class ServerPreferencesManager : IServerPreferencesManager, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IServerNetManager _netManager = 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 IDependencyCollection _dependencies = 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.
|
||||
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
|
||||
@@ -326,5 +322,12 @@ namespace Content.Server.Preferences.Managers
|
||||
public bool PrefsLoaded;
|
||||
public PlayerPreferences? Prefs;
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_userDb.AddOnLoadPlayer(LoadData);
|
||||
_userDb.AddOnFinishLoad(FinishLoad);
|
||||
_userDb.AddOnPlayerDisconnect(OnClientDisconnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Events;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Network;
|
||||
@@ -342,8 +343,9 @@ public sealed partial class StationJobsSystem
|
||||
foreach (var (player, profile) in profiles)
|
||||
{
|
||||
var roleBans = _banManager.GetJobBans(player);
|
||||
var profileJobs = profile.JobPriorities.Keys.ToList();
|
||||
_playTime.RemoveDisallowedJobs(player, ref profileJobs);
|
||||
var profileJobs = profile.JobPriorities.Keys.Select(k => new ProtoId<JobPrototype>(k)).ToList();
|
||||
var ev = new StationJobsGetCandidatesEvent(player, profileJobs);
|
||||
RaiseLocalEvent(ref ev);
|
||||
|
||||
List<string>? availableJobs = null;
|
||||
|
||||
@@ -354,7 +356,7 @@ public sealed partial class StationJobsSystem
|
||||
if (!(priority == selectedPriority || selectedPriority is null))
|
||||
continue;
|
||||
|
||||
if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job))
|
||||
if (!_prototypeManager.TryIndex(jobId, out var job))
|
||||
continue;
|
||||
|
||||
if (weight is not null && job.Weight != weight.Value)
|
||||
|
||||
@@ -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="disallowedJobs">A set of disallowed jobs, if any.</param>
|
||||
/// <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)
|
||||
return null;
|
||||
|
||||
@@ -225,6 +225,12 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<bool>
|
||||
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>
|
||||
/// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect.
|
||||
/// </summary>
|
||||
|
||||
33
Content.Shared/Players/JobWhitelist/MsgJobWhitelist.cs
Normal file
33
Content.Shared/Players/JobWhitelist/MsgJobWhitelist.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Shared.Roles
|
||||
{
|
||||
@@ -116,6 +114,9 @@ namespace Content.Shared.Roles
|
||||
|
||||
[DataField("extendedAccessGroups")]
|
||||
public IReadOnlyCollection<ProtoId<AccessGroupPrototype>> ExtendedAccessGroups { get; private set; } = Array.Empty<ProtoId<AccessGroupPrototype>>();
|
||||
|
||||
[DataField]
|
||||
public bool Whitelisted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
20
Resources/Locale/en-US/commands/job-whitelist-command.ftl
Normal file
20
Resources/Locale/en-US/commands/job-whitelist-command.ftl
Normal 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}).
|
||||
1
Resources/Locale/en-US/job/role-whitelist.ftl
Normal file
1
Resources/Locale/en-US/job/role-whitelist.ftl
Normal file
@@ -0,0 +1 @@
|
||||
role-not-whitelisted = You are not whitelisted to play this role.
|
||||
Reference in New Issue
Block a user