This commit is contained in:
ShadowCommander
2022-02-21 14:11:39 -08:00
committed by GitHub
parent fd8d9d2953
commit 4825142210
25 changed files with 3429 additions and 130 deletions

View File

@@ -0,0 +1,49 @@
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class JobBanCommand : IConsoleCommand
{
public string Command => "jobban";
public string Description => "Bans a player from a job";
public string Help => $"Usage: {Command} <name or user ID> <job> <reason> [duration in minutes, leave out or 0 for permanent ban]";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
string target;
string job;
string reason;
uint minutes;
switch (args.Length)
{
case 3:
target = args[0];
job = args[1];
reason = args[2];
minutes = 0;
break;
case 4:
target = args[0];
job = args[1];
reason = args[2];
if (!uint.TryParse(args[3], out minutes))
{
shell.WriteLine($"{args[3]} is not a valid amount of minutes.\n{Help}");
return;
}
break;
default:
shell.WriteLine($"Invalid amount of arguments.");
shell.WriteLine(Help);
return;
}
IoCManager.Resolve<RoleBanManager>().CreateJobBan(shell, target, job, reason, minutes);
}
}

View File

@@ -0,0 +1,84 @@
using System.Text;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class RoleBanListCommand : IConsoleCommand
{
public string Command => "rolebanlist";
public string Description => "Lists the user's role bans";
public string Help => "Usage: <name or user ID> [include unbanned]";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1 && args.Length != 2)
{
shell.WriteLine($"Invalid amount of args. {Help}");
return;
}
var includeUnbanned = true;
if (args.Length == 2 && !bool.TryParse(args[1], out includeUnbanned))
{
shell.WriteLine($"Argument two ({args[1]}) is not a boolean.");
return;
}
var dbMan = IoCManager.Resolve<IServerDbManager>();
var target = args[0];
var locator = IoCManager.Resolve<IPlayerLocator>();
var located = await locator.LookupIdByNameOrIdAsync(target);
if (located == null)
{
shell.WriteError("Unable to find a player with that name or id.");
return;
}
var targetUid = located.UserId;
var targetHWid = located.LastHWId;
var targetAddress = located.LastAddress;
var bans = await dbMan.GetServerRoleBansAsync(targetAddress, targetUid, targetHWid, includeUnbanned);
if (bans.Count == 0)
{
shell.WriteLine("That user has no bans in their record.");
return;
}
var bansString = new StringBuilder("Bans in record:\n");
foreach (var ban in bans)
{
bansString
.Append("Ban ID: ")
.Append(ban.Id)
.Append("\n")
.Append("Banned on ")
.Append(ban.BanTime);
if (ban.ExpirationTime != null)
{
bansString
.Append(" until ")
.Append(ban.ExpirationTime.Value);
}
bansString
.Append(".")
.Append("\n");
bansString
.Append("Reason: ")
.Append(ban.Reason)
.Append('\n');
}
shell.WriteLine(bansString.ToString());
}
}

View File

@@ -0,0 +1,183 @@
using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Managers;
public sealed class RoleBanManager
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
private const string JobPrefix = "Job:";
private readonly Dictionary<NetUserId, HashSet<ServerRoleBanDef>> _cachedRoleBans = new();
public void Initialize()
{
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.Connected
|| _cachedRoleBans.ContainsKey(e.Session.UserId))
return;
var netChannel = e.Session.ConnectedClient;
await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, netChannel.UserData.HWId);
}
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
{
if (banDef.UserId != null)
{
if (!_cachedRoleBans.TryGetValue(banDef.UserId.Value, out var roleBans))
{
roleBans = new HashSet<ServerRoleBanDef>();
_cachedRoleBans.Add(banDef.UserId.Value, roleBans);
}
if (!roleBans.Contains(banDef))
roleBans.Add(banDef);
}
await _db.AddServerRoleBanAsync(banDef);
return true;
}
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
{
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)
{
var roleBans = await _db.GetServerRoleBansAsync(address, userId, hwId, false);
var userRoleBans = new HashSet<ServerRoleBanDef>();
foreach (var ban in roleBans)
{
userRoleBans.Add(ban);
}
_cachedRoleBans[userId] = userRoleBans;
}
public void Restart()
{
// Clear out players that have disconnected.
var toRemove = new List<NetUserId>();
foreach (var player in _cachedRoleBans.Keys)
{
if (!_playerManager.TryGetSessionById(player, out _))
toRemove.Add(player);
}
foreach (var player in toRemove)
{
_cachedRoleBans.Remove(player);
}
// Check for expired bans
foreach (var (_, roleBans) in _cachedRoleBans)
{
roleBans.RemoveWhere(ban => DateTimeOffset.Now > ban.ExpirationTime);
}
}
#region Job Bans
public async void CreateJobBan(IConsoleShell shell, string target, string job, string reason, uint minutes)
{
if (!_prototypeManager.TryIndex(job, out JobPrototype? _))
{
shell.WriteLine($"Job {job} does not exist.");
return;
}
job = string.Concat(JobPrefix, job);
CreateRoleBan(shell, target, job, reason, minutes);
}
public HashSet<string>? GetJobBans(NetUserId playerUserId)
{
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
return null;
return roleBans
.Where(ban => ban.Role.StartsWith(JobPrefix))
.Select(ban => ban.Role[JobPrefix.Length..])
.ToHashSet();
}
#endregion
#region Commands
private async void CreateRoleBan(IConsoleShell shell, string target, string role, string reason, uint minutes)
{
var located = await _playerLocator.LookupIdByNameOrIdAsync(target);
if (located == null)
{
shell.WriteError("Unable to find a player with that name.");
return;
}
var targetUid = located.UserId;
var targetHWid = located.LastHWId;
var targetAddress = located.LastAddress;
DateTimeOffset? expires = null;
if (minutes > 0)
{
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes);
}
(IPAddress, int)? addressRange = null;
if (targetAddress != null)
{
if (targetAddress.IsIPv4MappedToIPv6)
targetAddress = targetAddress.MapToIPv4();
// Ban /64 for IPv4, /32 for IPv4.
var cidr = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? 64 : 32;
addressRange = (targetAddress, cidr);
}
var player = shell.Player as IPlayerSession;
var banDef = new ServerRoleBanDef(
null,
targetUid,
addressRange,
targetHWid,
DateTimeOffset.Now,
expires,
reason,
player?.UserId,
null,
role);
if (!await AddRoleBan(banDef))
{
shell.WriteLine($"{target} already has a role ban for {role}");
return;
}
var response = new StringBuilder($"Role banned {target} with reason \"{reason}\"");
response.Append(expires == null ?
" permanently."
: $" until {expires}");
shell.WriteLine(response.ToString());
}
#endregion
}

View File

@@ -306,6 +306,37 @@ namespace Content.Server.Database
public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
#endregion
#region Role Bans
/*
* ROLE BANS
*/
/// <summary>
/// Looks up a role ban by id.
/// This will return a pardoned role ban as well.
/// </summary>
/// <param name="id">The role ban id to look for.</param>
/// <returns>The role ban with the given id or null if none exist.</returns>
public abstract Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
/// <summary>
/// Looks up an user's role ban history.
/// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
/// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
/// </summary>
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned);
public abstract Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
#endregion
#region Player Records
/*
* PLAYER RECORDS

View File

@@ -90,6 +90,35 @@ namespace Content.Server.Database
Task AddServerUnbanAsync(ServerUnbanDef serverBan);
#endregion
#region Role Bans
/// <summary>
/// Looks up a role ban by id.
/// This will return a pardoned role ban as well.
/// </summary>
/// <param name="id">The role ban id to look for.</param>
/// <returns>The role ban with the given id or null if none exist.</returns>
Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
/// <summary>
/// Looks up an user's role ban history.
/// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
/// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
/// </summary>
/// <param name="address">The IP address of the user.</param>
/// <param name="userId">The NetUserId of the user.</param>
/// <param name="hwId">The Hardware Id of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned = true);
Task AddServerRoleBanAsync(ServerRoleBanDef serverBan);
Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
#endregion
#region Player Records
Task UpdatePlayerRecordAsync(
NetUserId userId,
@@ -264,6 +293,32 @@ namespace Content.Server.Database
return _db.AddServerUnbanAsync(serverUnban);
}
#region Role Ban
public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
return _db.GetServerRoleBanAsync(id);
}
public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned = true)
{
return _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned);
}
public Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
{
return _db.AddServerRoleBanAsync(serverRoleBan);
}
public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
{
return _db.AddServerRoleUnbanAsync(serverRoleUnban);
}
#endregion
public Task UpdatePlayerRecordAsync(
NetUserId userId,
string userName,

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Immutable;
using System.Data;
using System.Linq;
using System.Net;
@@ -34,6 +32,7 @@ namespace Content.Server.Database
});
}
#region Ban
public override async Task<ServerBanDef?> GetServerBanAsync(int id)
{
await using var db = await GetDbImpl();
@@ -52,9 +51,9 @@ namespace Content.Server.Database
NetUserId? userId,
ImmutableArray<byte>? hwId)
{
if (address == null && userId == null)
if (address == null && userId == null && hwId == null)
{
throw new ArgumentException("Address and userId cannot both be null");
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
@@ -73,7 +72,7 @@ namespace Content.Server.Database
{
if (address == null && userId == null && hwId == null)
{
throw new ArgumentException("Address and userId cannot both be null");
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
@@ -225,6 +224,191 @@ namespace Content.Server.Database
await db.PgDbContext.SaveChangesAsync();
}
#endregion
#region Role Ban
public override async Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
await using var db = await GetDbImpl();
var query = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(p => p.Id == id);
var ban = await query.SingleOrDefaultAsync();
return ConvertRoleBan(ban);
}
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
{
throw new ArgumentException("Address, userId, and hwId cannot all be null");
}
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned)
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
}
private static async Task<List<ServerRoleBanDef>> QueryRoleBans(IQueryable<ServerRoleBan> query)
{
var queryRoleBans = await query.ToArrayAsync();
var bans = new List<ServerRoleBanDef>(queryRoleBans.Length);
foreach (var ban in queryRoleBans)
{
var banDef = ConvertRoleBan(ban);
if (banDef != null)
{
bans.Add(banDef);
}
}
return bans;
}
private static IQueryable<ServerRoleBan> MakeRoleBanLookupQuery(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
DbGuardImpl db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.UserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
if (address != null)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (!includeUnbanned)
{
query = query?.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now));
}
query = query!.Distinct();
return query;
}
private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban)
{
if (ban == null)
{
return null;
}
NetUserId? uid = null;
if (ban.UserId is {} guid)
{
uid = new NetUserId(guid);
}
NetUserId? aUid = null;
if (ban.BanningAdmin is {} aGuid)
{
aUid = new NetUserId(aGuid);
}
var unbanDef = ConvertRoleUnban(ban.Unban);
return new ServerRoleBanDef(
ban.Id,
uid,
ban.Address,
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.BanTime,
ban.ExpirationTime,
ban.Reason,
aUid,
unbanDef,
ban.RoleId);
}
private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban)
{
if (unban == null)
{
return null;
}
NetUserId? aUid = null;
if (unban.UnbanningAdmin is {} aGuid)
{
aUid = new NetUserId(aGuid);
}
return new ServerRoleUnbanDef(
unban.Id,
aUid,
unban.UnbanTime);
}
public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
{
await using var db = await GetDbImpl();
db.PgDbContext.RoleBan.Add(new ServerRoleBan
{
Address = serverRoleBan.Address,
HWId = serverRoleBan.HWId?.ToArray(),
Reason = serverRoleBan.Reason,
BanningAdmin = serverRoleBan.BanningAdmin?.UserId,
BanTime = serverRoleBan.BanTime.UtcDateTime,
ExpirationTime = serverRoleBan.ExpirationTime?.UtcDateTime,
UserId = serverRoleBan.UserId?.UserId,
RoleId = serverRoleBan.Role,
});
await db.PgDbContext.SaveChangesAsync();
}
public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
{
await using var db = await GetDbImpl();
db.PgDbContext.RoleUnban.Add(new ServerRoleUnban
{
BanId = serverRoleUnban.BanId,
UnbanningAdmin = serverRoleUnban.UnbanningAdmin?.UserId,
UnbanTime = serverRoleUnban.UnbanTime.UtcDateTime
});
await db.PgDbContext.SaveChangesAsync();
}
#endregion
protected override PlayerRecord MakePlayerRecord(Player record)
{

View File

@@ -46,6 +46,7 @@ namespace Content.Server.Database
}
}
#region Ban
public override async Task<ServerBanDef?> GetServerBanAsync(int id)
{
await using var db = await GetDbImpl();
@@ -159,6 +160,162 @@ namespace Content.Server.Database
await db.SqliteDbContext.SaveChangesAsync();
}
#endregion
#region Role Ban
public override async Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
await using var db = await GetDbImpl();
var ban = await db.SqliteDbContext.RoleBan
.Include(p => p.Unban)
.Where(p => p.Id == id)
.SingleOrDefaultAsync();
return ConvertRoleBan(ban);
}
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
return queryBans
.Where(b => BanMatches(b, address, userId, hwId))
.Select(ConvertRoleBan)
.ToList()!;
}
private static async Task<List<ServerRoleBan>> GetAllRoleBans(
SqliteServerDbContext db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan> query = db.RoleBan.Include(p => p.Unban);
if (!includeUnbanned)
{
query = query.Where(p =>
p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow));
}
return await query.ToListAsync();
}
private static bool BanMatches(
ServerRoleBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
{
if (address != null && ban.Address is not null && IPAddressExt.IsInSubnet(address, ban.Address.Value))
{
return true;
}
if (userId is { } id && ban.UserId == id.UserId)
{
return true;
}
if (hwId is { } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId))
{
return true;
}
return false;
}
public override async Task AddServerRoleBanAsync(ServerRoleBanDef serverBan)
{
await using var db = await GetDbImpl();
db.SqliteDbContext.RoleBan.Add(new ServerRoleBan
{
Address = serverBan.Address,
Reason = serverBan.Reason,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
UserId = serverBan.UserId?.UserId,
RoleId = serverBan.Role,
});
await db.SqliteDbContext.SaveChangesAsync();
}
public override async Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverUnban)
{
await using var db = await GetDbImpl();
db.SqliteDbContext.RoleUnban.Add(new ServerRoleUnban
{
BanId = serverUnban.BanId,
UnbanningAdmin = serverUnban.UnbanningAdmin?.UserId,
UnbanTime = serverUnban.UnbanTime.UtcDateTime
});
await db.SqliteDbContext.SaveChangesAsync();
}
private static ServerRoleBanDef? ConvertRoleBan(ServerRoleBan? ban)
{
if (ban == null)
{
return null;
}
NetUserId? uid = null;
if (ban.UserId is { } guid)
{
uid = new NetUserId(guid);
}
NetUserId? aUid = null;
if (ban.BanningAdmin is { } aGuid)
{
aUid = new NetUserId(aGuid);
}
var unban = ConvertRoleUnban(ban.Unban);
return new ServerRoleBanDef(
ban.Id,
uid,
ban.Address,
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.BanTime,
ban.ExpirationTime,
ban.Reason,
aUid,
unban,
ban.RoleId);
}
private static ServerRoleUnbanDef? ConvertRoleUnban(ServerRoleUnban? unban)
{
if (unban == null)
{
return null;
}
NetUserId? aUid = null;
if (unban.UnbanningAdmin is { } aGuid)
{
aUid = new NetUserId(aGuid);
}
return new ServerRoleUnbanDef(
unban.Id,
aUid,
unban.UnbanTime);
}
#endregion
protected override PlayerRecord MakePlayerRecord(Player record)
{

View File

@@ -0,0 +1,56 @@
using System.Collections.Immutable;
using System.Net;
using Robust.Shared.Network;
namespace Content.Server.Database;
public sealed class ServerRoleBanDef
{
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
public string Reason { get; }
public NetUserId? BanningAdmin { get; }
public ServerRoleUnbanDef? Unban { get; }
public string Role { get; }
public ServerRoleBanDef(
int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
string reason,
NetUserId? banningAdmin,
ServerRoleUnbanDef? unban,
string role)
{
if (userId == null && address == null && hwId == null)
{
throw new ArgumentException("Must have at least one of banned user, banned address or hardware ID");
}
if (address is {} addr && addr.Item1.IsIPv4MappedToIPv6)
{
// Fix IPv6-mapped IPv4 addresses
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
address = (addr.Item1.MapToIPv4(), addr.Item2 - 96);
}
Id = id;
UserId = userId;
Address = address;
HWId = hwId;
BanTime = banTime;
ExpirationTime = expirationTime;
Reason = reason;
BanningAdmin = banningAdmin;
Unban = unban;
Role = role;
}
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Network;
namespace Content.Server.Database;
public sealed class ServerRoleUnbanDef
{
public int BanId { get; }
public NetUserId? UnbanningAdmin { get; }
public DateTimeOffset UnbanTime { get; }
public ServerRoleUnbanDef(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
{
BanId = banId;
UnbanningAdmin = unbanningAdmin;
UnbanTime = unbanTime;
}
}

View File

@@ -126,6 +126,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IGameMapManager>().Initialize();
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
IoCManager.Resolve<IBqlQueryManager>().DoAutoRegistrations();
IoCManager.Resolve<RoleBanManager>().Initialize();
}
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.MobState.Components;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -18,6 +19,56 @@ namespace Content.Server.GameTicking
private GamePresetPrototype? _preset;
private bool StartPreset(IPlayerSession[] origReadyPlayers, bool force)
{
var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
RaiseLocalEvent(startAttempt);
if (!startAttempt.Cancelled)
return true;
var presetTitle = _preset != null ? Loc.GetString(_preset.ModeTitle) : string.Empty;
void FailedPresetRestart()
{
SendServerMessage(Loc.GetString("game-ticker-start-round-cannot-start-game-mode-restart",
("failedGameMode", presetTitle)));
RestartRound();
DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
}
if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
{
var oldPreset = _preset;
ClearGameRules();
SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset));
AddGamePresetRules();
StartGamePresetRules();
startAttempt.Uncancel();
RaiseLocalEvent(startAttempt);
_chatManager.DispatchServerAnnouncement(
Loc.GetString("game-ticker-start-round-cannot-start-game-mode-fallback",
("failedGameMode", presetTitle),
("fallbackMode", Loc.GetString(_preset!.ModeTitle))));
if (startAttempt.Cancelled)
{
FailedPresetRestart();
}
RefreshLateJoinAllowed();
}
else
{
FailedPresetRestart();
return false;
}
return true;
}
private void InitializeGamePreset()
{
SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset));

View File

@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -7,12 +6,10 @@ using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Server.Player;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
@@ -25,17 +22,18 @@ namespace Content.Server.GameTicking
[ViewVariables]
private readonly Dictionary<string, int> _spawnedPositions = new();
private Dictionary<IPlayerSession, (string, StationId)> AssignJobs(List<IPlayerSession> available,
private Dictionary<IPlayerSession, (string, StationId)> AssignJobs(List<IPlayerSession> availablePlayers,
Dictionary<NetUserId, HumanoidCharacterProfile> profiles)
{
var assigned = new Dictionary<IPlayerSession, (string, StationId)>();
List<(IPlayerSession, List<string>)> GetPlayersJobCandidates(bool heads, JobPriority i)
{
return available.Select(player =>
return availablePlayers.Select(player =>
{
var profile = profiles[player.UserId];
var roleBans = _roleBanManager.GetJobBans(player.UserId);
var availableJobs = profile.JobPriorities
.Where(j =>
{
@@ -53,6 +51,7 @@ namespace Content.Server.GameTicking
return priority == i;
})
.Where(p => roleBans != null && !roleBans.Contains(p.Key))
.Select(j => j.Key)
.ToList();
@@ -85,7 +84,7 @@ namespace Content.Server.GameTicking
}
}
available.RemoveAll(a => assigned.ContainsKey(a));
availablePlayers.RemoveAll(a => assigned.ContainsKey(a));
}
// Current strategy is to fill each station one by one.
@@ -106,14 +105,17 @@ namespace Content.Server.GameTicking
return assigned;
}
private string? PickBestAvailableJob(HumanoidCharacterProfile profile, StationId station)
private string? PickBestAvailableJob(IPlayerSession playerSession, HumanoidCharacterProfile profile,
StationId station)
{
var available = _stationSystem.StationInfo[station].JobList;
bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId)
{
var roleBans = _roleBanManager.GetJobBans(playerSession.UserId);
var filtered = profile.JobPriorities
.Where(p => p.Value == priority)
.Where(p => roleBans != null && !roleBans.Contains(p.Key))
.Select(p => p.Key)
.ToList();

View File

@@ -69,7 +69,7 @@ namespace Content.Server.GameTicking
[ViewVariables]
public int RoundId { get; private set; }
private void PreRoundSetup()
private void LoadMaps()
{
AddGamePresetRules();
@@ -192,6 +192,14 @@ namespace Content.Server.GameTicking
StartGamePresetRules();
RoundLengthMetric.Set(0);
var playerIds = _playersInLobby.Keys.Select(player => player.UserId.UserId).ToArray();
RoundId = await _db.AddNewRound(playerIds);
var startingEvent = new RoundStartingEvent();
RaiseLocalEvent(startingEvent);
List<IPlayerSession> readyPlayers;
if (LobbyEnabled)
{
@@ -203,13 +211,13 @@ namespace Content.Server.GameTicking
readyPlayers = _playersInLobby.Keys.ToList();
}
RoundLengthMetric.Set(0);
var playerIds = _playersInLobby.Keys.Select(player => player.UserId.UserId).ToArray();
RoundId = await _db.AddNewRound(playerIds);
var startingEvent = new RoundStartingEvent();
RaiseLocalEvent(startingEvent);
readyPlayers.RemoveAll(p =>
{
if (_roleBanManager.GetRoleBans(p.UserId) != null)
return false;
Logger.ErrorS("RoleBans", $"Role bans for player {p} {p.UserId} have not been loaded yet.");
return true;
});
// Get the profiles for each player for easier lookup.
var profiles = _prefsManager.GetSelectedProfilesForPlayers(
@@ -227,106 +235,13 @@ namespace Content.Server.GameTicking
var origReadyPlayers = readyPlayers.ToArray();
var startAttempt = new RoundStartAttemptEvent(origReadyPlayers, force);
RaiseLocalEvent(startAttempt);
var presetTitle = _preset != null ? Loc.GetString(_preset.ModeTitle) : string.Empty;
void FailedPresetRestart()
{
SendServerMessage(Loc.GetString("game-ticker-start-round-cannot-start-game-mode-restart",
("failedGameMode", presetTitle)));
RestartRound();
DelayStart(TimeSpan.FromSeconds(PresetFailedCooldownIncrease));
}
if (startAttempt.Cancelled)
{
if (_configurationManager.GetCVar(CCVars.GameLobbyFallbackEnabled))
{
var oldPreset = _preset;
ClearGameRules();
SetGamePreset(_configurationManager.GetCVar(CCVars.GameLobbyFallbackPreset));
AddGamePresetRules();
StartGamePresetRules();
startAttempt.Uncancel();
RaiseLocalEvent(startAttempt);
_chatManager.DispatchServerAnnouncement(
Loc.GetString("game-ticker-start-round-cannot-start-game-mode-fallback",
("failedGameMode", presetTitle),
("fallbackMode", Loc.GetString(_preset!.ModeTitle))));
if (startAttempt.Cancelled)
{
FailedPresetRestart();
}
RefreshLateJoinAllowed();
}
else
{
FailedPresetRestart();
return;
}
}
if (!StartPreset(origReadyPlayers, force))
return;
// MapInitialize *before* spawning players, our codebase is too shit to do it afterwards...
_mapManager.DoMapInitialize(DefaultMap);
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force));
var assignedJobs = AssignJobs(readyPlayers, profiles);
// For players without jobs, give them the overflow job if they have that set...
foreach (var player in origReadyPlayers)
{
if (assignedJobs.ContainsKey(player))
{
continue;
}
var profile = profiles[player.UserId];
if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow)
{
// Pick a random station
var stations = _stationSystem.StationInfo.Keys.ToList();
_robustRandom.Shuffle(stations);
if (stations.Count == 0)
{
assignedJobs.Add(player, (FallbackOverflowJob, StationId.Invalid));
continue;
}
foreach (var station in stations)
{
// Pick a random overflow job from that station
var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone();
_robustRandom.Shuffle(overflows);
// Stations with no overflow slots should simply get skipped over.
if (overflows.Count == 0)
continue;
// If the overflow exists, put them in as it.
assignedJobs.Add(player, (overflows[0], stations[0]));
}
}
}
// Spawn everybody in!
foreach (var (player, (job, station)) in assignedJobs)
{
SpawnPlayer(player, profiles[player.UserId], station, job, false);
}
RefreshLateJoinAllowed();
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.ToArray(), profiles, force));
SpawnPlayers(readyPlayers, origReadyPlayers, profiles, force);
_roundStartDateTime = DateTime.UtcNow;
RunLevel = GameRunLevel.InRound;
@@ -458,7 +373,7 @@ namespace Content.Server.GameTicking
RunLevel = GameRunLevel.PreRoundLobby;
LobbySong = _robustRandom.Pick(_lobbyMusicCollection.PickFiles).ToString();
ResettingCleanup();
PreRoundSetup();
LoadMaps();
if (!LobbyEnabled)
{
@@ -508,6 +423,8 @@ namespace Content.Server.GameTicking
_mapManager.Restart();
_roleBanManager.Restart();
// Clear up any game rules.
ClearGameRules();

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Content.Server.Access.Systems;
@@ -22,13 +20,10 @@ using Content.Shared.Roles;
using Content.Shared.Species;
using Content.Shared.Station;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
@@ -48,6 +43,71 @@ namespace Content.Server.GameTicking
// Mainly to avoid allocations.
private readonly List<EntityCoordinates> _possiblePositions = new();
private void SpawnPlayers(List<IPlayerSession> readyPlayers, IPlayerSession[] origReadyPlayers,
Dictionary<NetUserId, HumanoidCharacterProfile> profiles, bool force)
{
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force));
var assignedJobs = AssignJobs(readyPlayers, profiles);
AssignOverflowJobs(assignedJobs, origReadyPlayers, profiles);
// Spawn everybody in!
foreach (var (player, (job, station)) in assignedJobs)
{
SpawnPlayer(player, profiles[player.UserId], station, job, false);
}
RefreshLateJoinAllowed();
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.ToArray(), profiles, force));
}
private void AssignOverflowJobs(IDictionary<IPlayerSession, (string, StationId)> assignedJobs,
IPlayerSession[] origReadyPlayers, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles)
{
// For players without jobs, give them the overflow job if they have that set...
foreach (var player in origReadyPlayers)
{
if (assignedJobs.ContainsKey(player))
{
continue;
}
var profile = profiles[player.UserId];
if (profile.PreferenceUnavailable != PreferenceUnavailableMode.SpawnAsOverflow)
continue;
// Pick a random station
var stations = _stationSystem.StationInfo.Keys.ToList();
if (stations.Count == 0)
{
assignedJobs.Add(player, (FallbackOverflowJob, StationId.Invalid));
continue;
}
_robustRandom.Shuffle(stations);
foreach (var station in stations)
{
// Pick a random overflow job from that station
var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone();
_robustRandom.Shuffle(overflows);
// Stations with no overflow slots should simply get skipped over.
if (overflows.Count == 0)
continue;
// If the overflow exists, put them in as it.
assignedJobs.Add(player, (overflows[0], stations[0]));
break;
}
}
}
private void SpawnPlayer(IPlayerSession player, StationId station, string? jobId = null, bool lateJoin = true)
{
var character = GetPlayerProfile(player);
@@ -90,7 +150,7 @@ namespace Content.Server.GameTicking
}
// Pick best job best on prefs.
jobId ??= PickBestAvailableJob(character, station);
jobId ??= PickBestAvailableJob(player, character, station);
// If no job available, stay in lobby, or if no lobby spawn as observer
if (jobId is null)
{

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.CharacterAppearance.Systems;
using Content.Server.Chat.Managers;
using Content.Server.Ghost;
@@ -16,14 +17,12 @@ using Robust.Shared.Configuration;
#if EXCEPTION_TOLERANCE
using Robust.Shared.Exceptions;
#endif
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameTicking
{
@@ -99,5 +98,6 @@ namespace Content.Server.GameTicking
[Dependency] private readonly PDASystem _pdaSystem = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly GhostSystem _ghosts = default!;
[Dependency] private readonly RoleBanManager _roleBanManager = default!;
}
}

View File

@@ -53,6 +53,7 @@ namespace Content.Server.IoC
IoCManager.Register<IGameMapManager, GameMapManager>();
IoCManager.Register<IGamePrototypeLoadManager, GamePrototypeLoadManager>();
IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<RoleBanManager, RoleBanManager>();
}
}
}