Holy crap auth works (#2099)

* Holy crap auth works

* Fix some usages of UserID instead of UserName

* Refactor preferences.

They be non-async now. Also faster.

* Rename DbContext.

* Guest username assignment.

* Fix saving of profiles.

* Don't store data for guests.

* Fix generating invalid random colors.

* Don't allow dumb garbage for char preferences.

* Bans.

* Lol forgot to fill out the command description.

* Connection log.

* Rename all the tables and columns to be snake_case.

* Re-do migrations.

* Fixing tests and warnings.

* Update submodule
This commit is contained in:
Pieter-Jan Briers
2020-09-29 14:26:00 +02:00
committed by GitHub
parent 8a33e0a9bd
commit 66c8a68891
72 changed files with 4144 additions and 2642 deletions

View File

@@ -0,0 +1,57 @@
using System;
using System.Net;
using Content.Server.Database;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
using Robust.Shared.Network;
#nullable enable
namespace Content.Server.Administration
{
public sealed class BanCommand : IClientCommand
{
public string Command => "ban";
public string Description => "Bans somebody";
public string Help => "Usage: <name or user ID> <reason> <duration in minutes, or 0 for permanent ban>";
public async void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{
var plyMgr = IoCManager.Resolve<IPlayerManager>();
var dbMan = IoCManager.Resolve<IServerDbManager>();
var target = args[0];
var reason = args[1];
var duration = int.Parse(args[2]);
NetUserId targetUid;
if (plyMgr.TryGetSessionByUsername(target, out var targetSession))
{
targetUid = targetSession.UserId;
}
else if (Guid.TryParse(target, out var targetGuid))
{
targetUid = new NetUserId(targetGuid);
}
else
{
shell.SendText(player, "Unable to find user with that name.");
return;
}
DateTimeOffset? expires = null;
if (duration > 0)
{
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(duration);
}
await dbMan.AddServerBanAsync(new ServerBanDef(targetUid, null, DateTimeOffset.Now, expires, reason, player?.UserId));
if (plyMgr.TryGetSessionById(targetUid, out var targetPlayer))
{
targetPlayer.ConnectedClient.Disconnect("You've been banned. Tough shit.");
}
}
}
}

View File

@@ -165,10 +165,10 @@ namespace Content.Server.Chat
var msg = _netManager.CreateNetMessage<MsgChatMessage>();
msg.Channel = ChatChannel.OOC;
msg.Message = message;
msg.MessageWrap = $"OOC: {player.SessionId}: {{0}}";
msg.MessageWrap = $"OOC: {player.Name}: {{0}}";
_netManager.ServerSendToAll(msg);
_mommiLink.SendOOCMessage(player.SessionId.ToString(), message);
_mommiLink.SendOOCMessage(player.Name, message);
}
public void SendDeadChat(IPlayerSession player, string message)
@@ -210,7 +210,7 @@ namespace Content.Server.Chat
msg.Channel = ChatChannel.AdminChat;
msg.Message = message;
msg.MessageWrap = $"{Loc.GetString("ADMIN")}: {player.SessionId}: {{0}}";
msg.MessageWrap = $"{Loc.GetString("ADMIN")}: {player.Name}: {{0}}";
_netManager.ServerSendToMany(msg, clients.ToList());
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.Preferences;
using Content.Shared;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Network;
#nullable enable
namespace Content.Server
{
public interface IConnectionManager
{
void Initialize();
}
/// <summary>
/// Handles various duties like guest username assignment, bans, connection logs, etc...
/// </summary>
public sealed class ConnectionManager : IConnectionManager
{
[Dependency] private readonly IServerNetManager _netMgr = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public void Initialize()
{
_netMgr.Connecting += NetMgrOnConnecting;
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
// Approval-based IP bans disabled because they don't play well with Happy Eyeballs.
// _netMgr.HandleApprovalCallback = HandleApproval;
}
/*
private async Task<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
{
var ban = await _db.GetServerBanByIpAsync(eventArgs.Connection.RemoteEndPoint.Address);
if (ban != null)
{
var expires = "This is a permanent ban.";
if (ban.ExpirationTime is { } expireTime)
{
var duration = expireTime - ban.BanTime;
var utc = expireTime.ToUniversalTime();
expires = $"This ban is for {duration.TotalMinutes} minutes and will expire at {utc:f} UTC.";
}
var reason = $@"You, or another user of this computer or connection is banned from playing here.
The ban reason is: ""{ban.Reason}""
{expires}";
return NetApproval.Deny(reason);
}
return NetApproval.Allow();
}
*/
private async Task NetMgrOnConnecting(NetConnectingArgs e)
{
// Check if banned.
var addr = e.IP.Address;
var userId = e.UserId;
var ban = await _db.GetServerBanAsync(addr, userId);
if (ban != null)
{
var expires = "This is a permanent ban.";
if (ban.ExpirationTime is { } expireTime)
{
var duration = expireTime - ban.BanTime;
var utc = expireTime.ToUniversalTime();
expires = $"This ban is for {duration.TotalMinutes:N0} minutes and will expire at {utc:f} UTC.";
}
var reason = $@"You, or another user of this computer or connection, are banned from playing here.
The ban reason is: ""{ban.Reason}""
{expires}";
e.Deny(reason);
return;
}
if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
{
return;
}
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr);
await _db.AddConnectionLogAsync(userId, e.UserName, addr);
}
private async Task<NetUserId?> AssignUserIdCallback(string name)
{
if (!_cfg.GetCVar(CCVars.GamePersistGuests))
{
return null;
}
var userId = await _db.GetAssignedUserIdAsync(name);
if (userId != null)
{
return userId;
}
var assigned = new NetUserId(Guid.NewGuid());
await _db.AssignUserIdAsync(name, assigned);
return assigned;
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Net;
using Robust.Shared.Network;
#nullable enable
namespace Content.Server.Database
{
public sealed class ServerBanDef
{
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
public string Reason { get; }
public NetUserId? BanningAdmin { get; }
public ServerBanDef(NetUserId? userId, (IPAddress, int)? address, DateTimeOffset banTime, DateTimeOffset? expirationTime, string reason, NetUserId? banningAdmin)
{
if (userId == null && address == null)
{
throw new ArgumentException("Must have a banned user, banned address, or both.");
}
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);
}
UserId = userId;
Address = address;
BanTime = banTime;
ExpirationTime = expirationTime;
Reason = reason;
BanningAdmin = banningAdmin;
}
}
}

View File

@@ -0,0 +1,221 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Content.Shared.Preferences;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Maths;
using Robust.Shared.Network;
namespace Content.Server.Database
{
public abstract class ServerDbBase
{
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
{
await using var db = await GetDb();
var prefs = await db.DbContext
.Preference
.Include(p => p.Profiles).ThenInclude(h => h.Jobs)
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
if (prefs is null) return null;
var maxSlot = prefs.Profiles.Max(p => p.Slot)+1;
var profiles = new ICharacterProfile[maxSlot];
foreach (var profile in prefs.Profiles)
{
profiles[profile.Slot] = ConvertProfiles(profile);
}
return new PlayerPreferences
(
profiles,
prefs.SelectedCharacterSlot
);
}
public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
{
await using var db = await GetDb();
var prefs = await db.DbContext.Preference.SingleAsync(p => p.UserId == userId.UserId);
prefs.SelectedCharacterSlot = index;
await db.DbContext.SaveChangesAsync();
}
public async Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
{
if (profile is null)
{
await DeleteCharacterSlotAsync(userId, slot);
return;
}
await using var db = await GetDb();
if (!(profile is HumanoidCharacterProfile humanoid))
{
// TODO: Handle other ICharacterProfile implementations properly
throw new NotImplementedException();
}
var entity = ConvertProfiles(humanoid, slot);
var prefs = await db.DbContext
.Preference
.Include(p => p.Profiles)
.SingleAsync(p => p.UserId == userId.UserId);
var oldProfile = prefs
.Profiles
.SingleOrDefault(h => h.Slot == entity.Slot);
if (!(oldProfile is null))
{
prefs.Profiles.Remove(oldProfile);
}
prefs.Profiles.Add(entity);
await db.DbContext.SaveChangesAsync();
}
private async Task DeleteCharacterSlotAsync(NetUserId userId, int slot)
{
await using var db = await GetDb();
db.DbContext
.Preference
.Single(p => p.UserId == userId.UserId)
.Profiles
.RemoveAll(h => h.Slot == slot);
await db.DbContext.SaveChangesAsync();
}
public async Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
{
await using var db = await GetDb();
var profile = ConvertProfiles((HumanoidCharacterProfile) defaultProfile, 0);
var prefs = new Preference
{
UserId = userId.UserId,
SelectedCharacterSlot = 0
};
prefs.Profiles.Add(profile);
db.DbContext.Preference.Add(prefs);
await db.DbContext.SaveChangesAsync();
return new PlayerPreferences(new []{defaultProfile}, 0);
}
private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
{
var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority);
var antags = profile.Antags.Select(a => a.AntagName);
return new HumanoidCharacterProfile(
profile.CharacterName,
profile.Age,
profile.Sex == "Male" ? Sex.Male : Sex.Female,
new HumanoidCharacterAppearance
(
profile.HairName,
Color.FromHex(profile.HairColor),
profile.FacialHairName,
Color.FromHex(profile.FacialHairColor),
Color.FromHex(profile.EyeColor),
Color.FromHex(profile.SkinColor)
),
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToList()
);
}
private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot)
{
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
var entity = new Profile
{
CharacterName = humanoid.Name,
Age = humanoid.Age,
Sex = humanoid.Sex.ToString(),
HairName = appearance.HairStyleName,
HairColor = appearance.HairColor.ToHex(),
FacialHairName = appearance.FacialHairStyleName,
FacialHairColor = appearance.FacialHairColor.ToHex(),
EyeColor = appearance.EyeColor.ToHex(),
SkinColor = appearance.SkinColor.ToHex(),
Slot = slot,
PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable
};
entity.Jobs.AddRange(
humanoid.JobPriorities
.Where(j => j.Value != JobPriority.Never)
.Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
);
entity.Antags.AddRange(
humanoid.AntagPreferences
.Select(a => new Antag {AntagName = a})
);
return entity;
}
public async Task<NetUserId?> GetAssignedUserIdAsync(string name)
{
await using var db = await GetDb();
var assigned = await db.DbContext.AssignedUserId.SingleOrDefaultAsync(p => p.UserName == name);
return assigned?.UserId is { } g ? new NetUserId(g) : default(NetUserId?);
}
public async Task AssignUserIdAsync(string name, NetUserId netUserId)
{
await using var db = await GetDb();
db.DbContext.AssignedUserId.Add(new AssignedUserId
{
UserId = netUserId.UserId,
UserName = name
});
await db.DbContext.SaveChangesAsync();
}
/*
* BAN STUFF
*/
public abstract Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
/*
* PLAYER RECORDS
*/
public abstract Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address);
/*
* CONNECTION LOG
*/
public abstract Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
protected abstract Task<DbGuard> GetDb();
protected abstract class DbGuard : IAsyncDisposable
{
public abstract ServerDbContext DbContext { get; }
public abstract ValueTask DisposeAsync();
}
}
}

View File

@@ -0,0 +1,245 @@
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Content.Shared;
using Content.Shared.Preferences;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.Interfaces.Resources;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
using LogLevel = Robust.Shared.Log.LogLevel;
#nullable enable
namespace Content.Server.Database
{
public interface IServerDbManager
{
void Init();
// Preferences
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile profile, int slot);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
// Username assignment (for guest accounts, so they persist GUID)
Task AssignUserIdAsync(string name, NetUserId userId);
Task<NetUserId?> GetAssignedUserIdAsync(string name);
// Ban stuff
Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId);
Task AddServerBanAsync(ServerBanDef serverBan);
// Player records
Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address);
// Connection log
Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address);
}
public sealed class ServerDbManager : IServerDbManager
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _logMgr = default!;
private ServerDbBase _db = default!;
private LoggingProvider _msLogProvider = default!;
private ILoggerFactory _msLoggerFactory = default!;
public void Init()
{
_msLogProvider = new LoggingProvider(_logMgr);
_msLoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddProvider(_msLogProvider);
});
var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
switch (engine)
{
case "sqlite":
var options = CreateSqliteOptions();
_db = new ServerDbSqlite(options);
break;
case "postgres":
options = CreatePostgresOptions();
_db = new ServerDbPostgres(options);
break;
default:
throw new InvalidDataException("Unknown database engine {engine}.");
}
}
public Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
{
return _db.InitPrefsAsync(userId, defaultProfile);
}
public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
{
return _db.SaveSelectedCharacterIndexAsync(userId, index);
}
public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile profile, int slot)
{
return _db.SaveCharacterSlotAsync(userId, profile, slot);
}
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
{
return _db.GetPlayerPreferencesAsync(userId);
}
public Task AssignUserIdAsync(string name, NetUserId userId)
{
return _db.AssignUserIdAsync(name, userId);
}
public Task<NetUserId?> GetAssignedUserIdAsync(string name)
{
return _db.GetAssignedUserIdAsync(name);
}
public Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId)
{
return _db.GetServerBanAsync(address, userId);
}
public Task AddServerBanAsync(ServerBanDef serverBan)
{
return _db.AddServerBanAsync(serverBan);
}
public Task UpdatePlayerRecordAsync(NetUserId userId, string userName, IPAddress address)
{
return _db.UpdatePlayerRecord(userId, userName, address);
}
public Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
{
return _db.AddConnectionLogAsync(userId, userName, address);
}
private DbContextOptions<ServerDbContext> CreatePostgresOptions()
{
var host = _cfg.GetCVar(CCVars.DatabasePgHost);
var port = _cfg.GetCVar(CCVars.DatabasePgPort);
var db = _cfg.GetCVar(CCVars.DatabasePgDatabase);
var user = _cfg.GetCVar(CCVars.DatabasePgUsername);
var pass = _cfg.GetCVar(CCVars.DatabasePgPassword);
var builder = new DbContextOptionsBuilder<ServerDbContext>();
var connectionString = new NpgsqlConnectionStringBuilder
{
Host = host,
Port = port,
Database = db,
Username = user,
Password = pass
}.ConnectionString;
builder.UseNpgsql(connectionString);
SetupLogging(builder);
return builder.Options;
}
private DbContextOptions<ServerDbContext> CreateSqliteOptions()
{
var builder = new DbContextOptionsBuilder<ServerDbContext>();
var configPreferencesDbPath = _cfg.GetCVar(CCVars.DatabaseSqliteDbPath);
var inMemory = _res.UserData.RootDir == null;
SqliteConnection connection;
if (!inMemory)
{
var finalPreferencesDbPath = Path.Combine(_res.UserData.RootDir!, configPreferencesDbPath);
connection = new SqliteConnection($"Data Source={finalPreferencesDbPath}");
}
else
{
connection = new SqliteConnection("Data Source=:memory:");
// When using an in-memory DB we have to open it manually
// so EFCore doesn't open, close and wipe it.
connection.Open();
}
builder.UseSqlite(connection);
SetupLogging(builder);
return builder.Options;
}
private void SetupLogging(DbContextOptionsBuilder<ServerDbContext> builder)
{
builder.UseLoggerFactory(_msLoggerFactory);
}
private sealed class LoggingProvider : ILoggerProvider
{
private readonly ILogManager _logManager;
public LoggingProvider(ILogManager logManager)
{
_logManager = logManager;
}
public void Dispose()
{
}
public ILogger CreateLogger(string categoryName)
{
return new MSLogger(_logManager.GetSawmill("db.ef"));
}
}
private sealed class MSLogger : ILogger
{
private readonly ISawmill _sawmill;
public MSLogger(ISawmill sawmill)
{
_sawmill = sawmill;
}
public void Log<TState>(MSLogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
var lvl = logLevel switch
{
MSLogLevel.Trace => LogLevel.Debug,
MSLogLevel.Debug => LogLevel.Debug,
// EFCore feels the need to log individual DB commands as "Information" so I'm slapping debug on it.
MSLogLevel.Information => LogLevel.Debug,
MSLogLevel.Warning => LogLevel.Warning,
MSLogLevel.Error => LogLevel.Error,
MSLogLevel.Critical => LogLevel.Fatal,
MSLogLevel.None => LogLevel.Debug,
_ => LogLevel.Debug
};
_sawmill.Log(lvl, formatter(state, exception));
}
public bool IsEnabled(MSLogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
// TODO: this
return null!;
}
}
}
}

View File

@@ -0,0 +1,184 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Network;
#nullable enable
namespace Content.Server.Database
{
public sealed class ServerDbPostgres : ServerDbBase
{
private readonly DbContextOptions<ServerDbContext> _options;
private readonly Task _dbReadyTask;
public ServerDbPostgres(DbContextOptions<ServerDbContext> options)
{
_options = options;
_dbReadyTask = Task.Run(async () =>
{
await using var ctx = new PostgresServerDbContext(_options);
try
{
await ctx.Database.MigrateAsync();
}
finally
{
await ctx.DisposeAsync();
}
});
}
public override async Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId)
{
if (address == null && userId == null)
{
throw new ArgumentException("Address and userId cannot both be null");
}
await using var db = await GetDbImpl();
var query = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(p => p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.Now));
if (userId is { } uid)
{
if (address == null)
{
// Only have a user ID.
query = query.Where(p => p.UserId == uid.UserId);
}
else
{
// Have both user ID and IP address.
query = query.Where(p =>
(p.Address != null && EF.Functions.ContainsOrEqual(p.Address.Value, address))
|| p.UserId == uid.UserId);
}
}
else
{
// Only have a connecting address.
query = query.Where(
p => p.Address != null && EF.Functions.ContainsOrEqual(p.Address.Value, address));
}
var ban = await query.FirstOrDefaultAsync();
return ConvertBan(ban);
}
private static ServerBanDef? ConvertBan(PostgresServerBan? 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);
}
return new ServerBanDef(
uid,
ban.Address,
ban.BanTime,
ban.ExpirationTime,
ban.Reason,
aUid);
}
public override async Task AddServerBanAsync(ServerBanDef serverBan)
{
await using var db = await GetDbImpl();
db.PgDbContext.Ban.Add(new PostgresServerBan
{
Address = serverBan.Address,
Reason = serverBan.Reason,
BanningAdmin = serverBan.BanningAdmin?.UserId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
UserId = serverBan.UserId?.UserId
});
await db.PgDbContext.SaveChangesAsync();
}
public override async Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address)
{
await using var db = await GetDbImpl();
var record = await db.PgDbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
if (record == null)
{
db.PgDbContext.Player.Add(record = new PostgresPlayer
{
FirstSeenTime = DateTime.UtcNow,
UserId = userId.UserId,
});
}
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address;
record.LastSeenUserName = userName;
await db.PgDbContext.SaveChangesAsync();
}
public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
{
await using var db = await GetDbImpl();
db.PgDbContext.ConnectionLog.Add(new PostgresConnectionLog
{
Address = address,
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName
});
await db.PgDbContext.SaveChangesAsync();
}
private async Task<DbGuardImpl> GetDbImpl()
{
await _dbReadyTask;
return new DbGuardImpl(new PostgresServerDbContext(_options));
}
protected override async Task<DbGuard> GetDb()
{
return await GetDbImpl();
}
private sealed class DbGuardImpl : DbGuard
{
public DbGuardImpl(PostgresServerDbContext dbC)
{
PgDbContext = dbC;
}
public PostgresServerDbContext PgDbContext { get; }
public override ServerDbContext DbContext => PgDbContext;
public override ValueTask DisposeAsync()
{
return DbContext.DisposeAsync();
}
}
}
}

View File

@@ -0,0 +1,192 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Preferences;
using Content.Server.Utility;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Network;
#nullable enable
namespace Content.Server.Database
{
/// <summary>
/// Provides methods to retrieve and update character preferences.
/// Don't use this directly, go through <see cref="ServerPreferencesManager" /> instead.
/// </summary>
public sealed class ServerDbSqlite : ServerDbBase
{
// For SQLite we use a single DB context via SQLite.
// This doesn't allow concurrent access so that's what the semaphore is for.
// That said, this is bloody SQLite, I don't even think EFCore bothers to truly async it.
private readonly SemaphoreSlim _prefsSemaphore = new SemaphoreSlim(1, 1);
private readonly Task _dbReadyTask;
private readonly SqliteServerDbContext _prefsCtx;
public ServerDbSqlite(DbContextOptions<ServerDbContext> options)
{
_prefsCtx = new SqliteServerDbContext(options);
_dbReadyTask = Task.Run(() => _prefsCtx.Database.Migrate());
}
public override async Task<ServerBanDef?> GetServerBanAsync(IPAddress? address, NetUserId? userId)
{
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 bans = await db.SqliteDbContext.Ban
.Include(p => p.Unban)
.Where(p => p.Unban == null && (p.ExpirationTime == null || p.ExpirationTime.Value > DateTime.UtcNow))
.ToListAsync();
foreach (var ban in bans)
{
if (address != null && ban.Address != null && address.IsInSubnet(ban.Address))
{
return ConvertBan(ban);
}
if (userId is { } id && ban.UserId == id.UserId)
{
return ConvertBan(ban);
}
}
return null;
}
public override async Task AddServerBanAsync(ServerBanDef serverBan)
{
await using var db = await GetDbImpl();
string? addrStr = null;
if (serverBan.Address is { } addr)
{
addrStr = $"{addr.address}/{addr.cidrMask}";
}
db.SqliteDbContext.Ban.Add(new SqliteServerBan
{
Address = addrStr,
Reason = serverBan.Reason,
BanningAdmin = serverBan.BanningAdmin?.UserId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
UserId = serverBan.UserId?.UserId
});
await db.SqliteDbContext.SaveChangesAsync();
}
public override async Task UpdatePlayerRecord(NetUserId userId, string userName, IPAddress address)
{
await using var db = await GetDbImpl();
var record = await db.SqliteDbContext.Player.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
if (record == null)
{
db.SqliteDbContext.Player.Add(record = new SqlitePlayer
{
FirstSeenTime = DateTime.UtcNow,
UserId = userId.UserId,
});
}
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address.ToString();
record.LastSeenUserName = userName;
await db.SqliteDbContext.SaveChangesAsync();
}
private static ServerBanDef? ConvertBan(SqliteServerBan? 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);
}
(IPAddress, int)? addrTuple = null;
if (ban.Address != null)
{
var idx = ban.Address.IndexOf('/', StringComparison.Ordinal);
addrTuple = (IPAddress.Parse(ban.Address.AsSpan(0, idx)),
int.Parse(ban.Address.AsSpan(idx + 1), provider: CultureInfo.InvariantCulture));
}
return new ServerBanDef(
uid,
addrTuple,
ban.BanTime,
ban.ExpirationTime,
ban.Reason,
aUid);
}
public override async Task AddConnectionLogAsync(NetUserId userId, string userName, IPAddress address)
{
await using var db = await GetDbImpl();
db.SqliteDbContext.ConnectionLog.Add(new SqliteConnectionLog
{
Address = address.ToString(),
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName
});
await db.SqliteDbContext.SaveChangesAsync();
}
private async Task<DbGuardImpl> GetDbImpl()
{
await _dbReadyTask;
await _prefsSemaphore.WaitAsync();
return new DbGuardImpl(this);
}
protected override async Task<DbGuard> GetDb()
{
return await GetDbImpl();
}
private sealed class DbGuardImpl : DbGuard
{
private readonly ServerDbSqlite _db;
public DbGuardImpl(ServerDbSqlite db)
{
_db = db;
}
public override ServerDbContext DbContext => _db._prefsCtx;
public SqliteServerDbContext SqliteDbContext => _db._prefsCtx;
public override ValueTask DisposeAsync()
{
_db._prefsSemaphore.Release();
return default;
}
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState;
using Content.Server.Body.Network;
using Content.Server.Database;
using Content.Server.GameObjects.Components.Mobs.Speech;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.Interfaces;
@@ -62,7 +63,9 @@ namespace Content.Server
var logManager = IoCManager.Resolve<ILogManager>();
logManager.GetSawmill("Storage").Level = LogLevel.Info;
IoCManager.Resolve<IServerPreferencesManager>().StartInit();
IoCManager.Resolve<IConnectionManager>().Initialize();
IoCManager.Resolve<IServerDbManager>().Init();
IoCManager.Resolve<IServerPreferencesManager>().Init();
IoCManager.Resolve<INodeGroupFactory>().Initialize();
IoCManager.Resolve<ISandboxManager>().Initialize();
IoCManager.Resolve<IAccentManager>().Initialize();
@@ -72,7 +75,6 @@ namespace Content.Server
{
base.PostInit();
IoCManager.Resolve<IServerPreferencesManager>().FinishInit();
_gameTicker.Initialize();
IoCManager.Resolve<RecipeManager>().Initialize();
IoCManager.Resolve<BlackboardManager>().Initialize();

View File

@@ -22,6 +22,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
@@ -144,7 +145,7 @@ namespace Content.Server.GameObjects.Components.Medical
UserInterface?.Open(actor.playerSession);
}
private async void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (!(obj.Message is CloningPodUiButtonPressedMessage message)) return;
@@ -167,11 +168,10 @@ namespace Content.Server.GameObjects.Components.Medical
var mob = _entityManager.SpawnEntity("HumanMob_Content", Owner.Transform.MapPosition);
var client = _playerManager
.GetPlayersBy(x => x.SessionId == mind.SessionId).First();
mob.GetComponent<HumanoidAppearanceComponent>()
.UpdateFromProfile(GetPlayerProfileAsync(client.Name).Result);
mob.Name = GetPlayerProfileAsync(client.Name).Result.Name;
var client = _playerManager.GetSessionByUserId(mind.UserId!.Value);
var profile = GetPlayerProfileAsync(client.UserId);
mob.GetComponent<HumanoidAppearanceComponent>().UpdateFromProfile(profile);
mob.Name = profile.Name;
_bodyContainer.Insert(mob);
_capturedMind = mind;
@@ -209,10 +209,9 @@ namespace Content.Server.GameObjects.Components.Medical
}
private async Task<HumanoidCharacterProfile> GetPlayerProfileAsync(string username)
private HumanoidCharacterProfile GetPlayerProfileAsync(NetUserId userId)
{
return (HumanoidCharacterProfile) (await _prefsManager.GetPreferencesAsync(username))
.SelectedCharacter;
return (HumanoidCharacterProfile) _prefsManager.GetPreferences(userId).SelectedCharacter;
}
private void HandleGhostReturn(GhostComponent.GhostReturnMessage message)

View File

@@ -16,16 +16,16 @@ namespace Content.Server.GameObjects.EntitySystems
{
public class SignalLinkerSystem : EntitySystem
{
private Dictionary<NetSessionId, SignalTransmitterComponent> _transmitters;
private Dictionary<NetUserId, SignalTransmitterComponent> _transmitters;
public override void Initialize()
{
base.Initialize();
_transmitters = new Dictionary<NetSessionId, SignalTransmitterComponent>();
_transmitters = new Dictionary<NetUserId, SignalTransmitterComponent>();
}
public void SignalLinkerKeybind(NetSessionId id, bool? enable)
public void SignalLinkerKeybind(NetUserId id, bool? enable)
{
if (enable == null)
{
@@ -66,7 +66,7 @@ namespace Content.Server.GameObjects.EntitySystems
private bool HandleUse(ICommonSession session, EntityCoordinates coords, EntityUid uid)
{
if (!_transmitters.TryGetValue(session.SessionId, out var signalTransmitter))
if (!_transmitters.TryGetValue(session.UserId, out var signalTransmitter))
{
return false;
}
@@ -86,7 +86,7 @@ namespace Content.Server.GameObjects.EntitySystems
if (entity.TryGetComponent<SignalTransmitterComponent>(out var transmitter))
{
_transmitters[session.SessionId] = transmitter.GetSignal(session.AttachedEntity);
_transmitters[session.UserId] = transmitter.GetSignal(session.AttachedEntity);
return true;
}
@@ -129,7 +129,7 @@ namespace Content.Server.GameObjects.EntitySystems
return;
}
system.SignalLinkerKeybind(player.SessionId, enable);
system.SignalLinkerKeybind(player.UserId, enable);
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Content.Shared.Preferences;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Network;
namespace Content.Server.GameTicking
{
@@ -13,6 +14,6 @@ namespace Content.Server.GameTicking
public virtual string ModeTitle => "Sandbox";
public virtual string Description => "Secret!";
public virtual bool DisallowLateJoin => false;
public Dictionary<string, HumanoidCharacterProfile> readyProfiles;
public Dictionary<NetUserId, HumanoidCharacterProfile> readyProfiles;
}
}

View File

@@ -76,11 +76,11 @@ namespace Content.Server.GameTicking.GamePresets
foreach (var player in list)
{
if (!readyProfiles.ContainsKey(player.Name))
if (!readyProfiles.ContainsKey(player.UserId))
{
continue;
}
var profile = readyProfiles[player.Name];
var profile = readyProfiles[player.UserId];
if (profile.AntagPreferences.Contains(_prototypeManager.Index<AntagPrototype>(TraitorID).Name))
{
prefList.Add(player);

View File

@@ -6,6 +6,7 @@ using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -19,7 +20,7 @@ namespace Content.Server.GameTicking
private readonly Dictionary<string, int> _spawnedPositions = new Dictionary<string, int>();
private Dictionary<IPlayerSession, string> AssignJobs(List<IPlayerSession> available,
Dictionary<string, HumanoidCharacterProfile> profiles)
Dictionary<NetUserId, HumanoidCharacterProfile> profiles)
{
// Calculate positions available round-start for each job.
var availablePositions = GetBasePositions(true);
@@ -38,7 +39,7 @@ namespace Content.Server.GameTicking
var candidates = available
.Select(player =>
{
var profile = profiles[player.Name];
var profile = profiles[player.UserId];
var availableJobs = profile.JobPriorities
.Where(j =>

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage;
@@ -34,7 +33,6 @@ using Robust.Server.Interfaces.Maps;
using Robust.Server.Interfaces.Player;
using Robust.Server.Player;
using Robust.Server.ServerStatus;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
@@ -222,7 +220,7 @@ namespace Content.Server.GameTicking
}
}
public async void StartRound(bool force = false)
public void StartRound(bool force = false)
{
DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby);
Logger.InfoS("ticker", "Starting round!");
@@ -244,16 +242,16 @@ namespace Content.Server.GameTicking
RoundLengthMetric.Set(0);
// Get the profiles for each player for easier lookup.
var profiles = (await _prefsManager.GetSelectedProfilesForPlayersAsync(
var profiles = _prefsManager.GetSelectedProfilesForPlayers(
readyPlayers
.Select(p => p.Name).ToList()))
.Select(p => p.UserId).ToList())
.ToDictionary(p => p.Key, p => (HumanoidCharacterProfile) p.Value);
foreach (var readyPlayer in readyPlayers)
{
if (!profiles.ContainsKey(readyPlayer.Name))
if (!profiles.ContainsKey(readyPlayer.UserId))
{
profiles.Add(readyPlayer.Name, HumanoidCharacterProfile.Default());
profiles.Add(readyPlayer.UserId, HumanoidCharacterProfile.Default());
}
}
@@ -267,7 +265,7 @@ namespace Content.Server.GameTicking
continue;
}
var profile = profiles[player.Name];
var profile = profiles[player.UserId];
if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow)
{
assignedJobs.Add(player, OverflowJob);
@@ -277,7 +275,7 @@ namespace Content.Server.GameTicking
// Spawn everybody in!
foreach (var (player, job) in assignedJobs)
{
SpawnPlayer(player, profiles[player.Name], job, false);
SpawnPlayer(player, profiles[player.UserId], job, false);
}
// Time to start the preset.
@@ -320,9 +318,10 @@ namespace Content.Server.GameTicking
IoCManager.Resolve<IServerNetManager>().ServerSendToAll(msg);
}
private async Task<HumanoidCharacterProfile> GetPlayerProfileAsync(IPlayerSession p) =>
(HumanoidCharacterProfile) (await _prefsManager.GetPreferencesAsync(p.SessionId.Username))
.SelectedCharacter;
private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p)
{
return (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.UserId).SelectedCharacter;
}
public void EndRound(string roundEndText = "")
{
@@ -373,7 +372,7 @@ namespace Content.Server.GameTicking
if (LobbyEnabled)
_playerJoinLobby(targetPlayer);
else
SpawnPlayerAsync(targetPlayer);
SpawnPlayer(targetPlayer);
}
public void MakeObserve(IPlayerSession player)
@@ -389,13 +388,23 @@ namespace Content.Server.GameTicking
{
if (!_playersInLobby.ContainsKey(player)) return;
SpawnPlayerAsync(player, jobId);
if (!_prefsManager.HavePreferencesLoaded(player))
{
return;
}
SpawnPlayer(player, jobId);
}
public void ToggleReady(IPlayerSession player, bool ready)
{
if (!_playersInLobby.ContainsKey(player)) return;
if (!_prefsManager.HavePreferencesLoaded(player))
{
return;
}
var status = ready ? PlayerStatus.Ready : PlayerStatus.NotReady;
_playersInLobby[player] = ready ? PlayerStatus.Ready : PlayerStatus.NotReady;
_netManager.ServerSendMessage(_getStatusMsg(player), player.ConnectedClient);
@@ -703,7 +712,7 @@ namespace Content.Server.GameTicking
case SessionStatus.Connected:
{
_chatManager.DispatchServerAnnouncement($"Player {args.Session.SessionId} joined server!");
_chatManager.DispatchServerAnnouncement($"Player {args.Session.Name} joined server!");
if (LobbyEnabled && _roundStartCountdownHasNotStartedYetDueToNoPlayers)
{
@@ -716,6 +725,8 @@ namespace Content.Server.GameTicking
case SessionStatus.InGame:
{
_prefsManager.OnClientConnected(session);
var data = session.ContentData();
if (data.Mind == null)
{
@@ -725,13 +736,14 @@ namespace Content.Server.GameTicking
return;
}
SpawnPlayerAsync(session);
SpawnWaitPrefs();
}
else
{
if (data.Mind.CurrentEntity == null)
{
SpawnPlayerAsync(session);
SpawnWaitPrefs();
}
else
{
@@ -747,11 +759,18 @@ namespace Content.Server.GameTicking
{
if (_playersInLobby.ContainsKey(session)) _playersInLobby.Remove(session);
_chatManager.DispatchServerAnnouncement($"Player {args.Session.SessionId} left server!");
_chatManager.DispatchServerAnnouncement($"Player {args.Session} left server!");
ServerEmptyUpdateRestartCheck();
_prefsManager.OnClientDisconnected(session);
break;
}
}
async void SpawnWaitPrefs()
{
await _prefsManager.WaitPreferencesLoaded(session);
SpawnPlayer(session);
}
}
/// <summary>
@@ -785,11 +804,9 @@ namespace Content.Server.GameTicking
}, _updateShutdownCts.Token);
}
private async void SpawnPlayerAsync(IPlayerSession session, string jobId = null, bool lateJoin = true)
private void SpawnPlayer(IPlayerSession session, string jobId = null, bool lateJoin = true)
{
var character = (HumanoidCharacterProfile) (await _prefsManager
.GetPreferencesAsync(session.SessionId.Username))
.SelectedCharacter;
var character = GetPlayerProfile(session);
SpawnPlayer(session, character, jobId, lateJoin);
}
@@ -809,7 +826,7 @@ namespace Content.Server.GameTicking
var data = session.ContentData();
data.WipeMind();
data.Mind = new Mind(session.SessionId)
data.Mind = new Mind(session.UserId)
{
CharacterName = character.Name
};
@@ -866,17 +883,15 @@ namespace Content.Server.GameTicking
_manifest.Add(new ManifestEntry(characterName, jobId));
}
private async void _spawnObserver(IPlayerSession session)
private void _spawnObserver(IPlayerSession session)
{
_playerJoinGame(session);
var name = (await _prefsManager
.GetPreferencesAsync(session.SessionId.Username))
.SelectedCharacter.Name;
var name = GetPlayerProfile(session).Name;
var data = session.ContentData();
data.WipeMind();
data.Mind = new Mind(session.SessionId);
data.Mind = new Mind(session.UserId);
var mob = _spawnObserverMob();
mob.Name = name;
@@ -888,7 +903,6 @@ namespace Content.Server.GameTicking
{
_playersInLobby.Add(session, PlayerStatus.NotReady);
_prefsManager.OnClientConnected(session);
_netManager.ServerSendMessage(_netManager.CreateNetMessage<MsgTickerJoinLobby>(), session.ConnectedClient);
_netManager.ServerSendMessage(_getStatusMsg(session), session.ConnectedClient);
_netManager.ServerSendMessage(GetInfoMsg(), session.ConnectedClient);
@@ -907,11 +921,11 @@ namespace Content.Server.GameTicking
private MsgTickerLobbyReady GetPlayerStatus()
{
var msg = _netManager.CreateNetMessage<MsgTickerLobbyReady>();
msg.PlayerStatus = new Dictionary<NetSessionId, PlayerStatus>();
msg.PlayerStatus = new Dictionary<NetUserId, PlayerStatus>();
foreach (var player in _playersInLobby.Keys)
{
_playersInLobby.TryGetValue(player, out var status);
msg.PlayerStatus.Add(player.SessionId, status);
msg.PlayerStatus.Add(player.UserId, status);
}
return msg;
}
@@ -919,9 +933,9 @@ namespace Content.Server.GameTicking
private MsgTickerLobbyReady GetStatusSingle(IPlayerSession player, PlayerStatus status)
{
var msg = _netManager.CreateNetMessage<MsgTickerLobbyReady>();
msg.PlayerStatus = new Dictionary<NetSessionId, PlayerStatus>
msg.PlayerStatus = new Dictionary<NetUserId, PlayerStatus>
{
{ player.SessionId, status }
{ player.UserId, status }
};
return msg;
}
@@ -967,7 +981,7 @@ The current game mode is: [color=white]{0}[/color].
_netManager.ServerSendToMany(infoMsg, _playersInLobby.Keys.Select(p => p.ConnectedClient).ToList());
}
private GamePreset MakeGamePreset(Dictionary<string, HumanoidCharacterProfile> readyProfiles)
private GamePreset MakeGamePreset(Dictionary<NetUserId, HumanoidCharacterProfile> readyProfiles)
{
var preset = _dynamicTypeFactory.CreateInstance<GamePreset>(_presetType ?? typeof(PresetSandbox));
preset.readyProfiles = readyProfiles;

View File

@@ -31,7 +31,7 @@ namespace Content.Server.GameTicking
{
// Always make sure the client has player data. Mind gets assigned on spawn.
if (session.Data.ContentDataUncast == null)
session.Data.ContentDataUncast = new PlayerData(session.SessionId);
session.Data.ContentDataUncast = new PlayerData(session.UserId);
// timer time must be > tick length
Timer.Spawn(0, args.Session.JoinGame);

View File

@@ -130,7 +130,7 @@ namespace Content.Server.GameTicking
var playerMgr = IoCManager.Resolve<IPlayerManager>();
var ticker = IoCManager.Resolve<IGameTicker>();
NetSessionId sessionId;
NetUserId userId;
if (args.Length == 0)
{
if (player == null)
@@ -139,16 +139,17 @@ namespace Content.Server.GameTicking
return;
}
sessionId = player.SessionId;
userId = player.UserId;
}
else
else if (!playerMgr.TryGetUserId(args[0], out userId))
{
sessionId = new NetSessionId(args[0]);
shell.SendText(player, "Unknown player");
return;
}
if (!playerMgr.TryGetSessionById(sessionId, out var targetPlayer))
if (!playerMgr.TryGetSessionById(userId, out var targetPlayer))
{
if (!playerMgr.TryGetPlayerData(sessionId, out var data))
if (!playerMgr.TryGetPlayerData(userId, out var data))
{
shell.SendText(player, "Unknown player");
return;

View File

@@ -2,15 +2,21 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Shared.Preferences;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Network;
namespace Content.Server.Interfaces
{
public interface IServerPreferencesManager
{
void FinishInit();
void Init();
void OnClientConnected(IPlayerSession session);
Task<PlayerPreferences> GetPreferencesAsync(string username);
Task<IEnumerable<KeyValuePair<string, ICharacterProfile>>> GetSelectedProfilesForPlayersAsync(List<string> usernames);
void StartInit();
void OnClientDisconnected(IPlayerSession session);
bool HavePreferencesLoaded(IPlayerSession session);
Task WaitPreferencesLoaded(IPlayerSession session);
PlayerPreferences GetPreferences(NetUserId userId);
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
}
}

View File

@@ -29,12 +29,12 @@ namespace Content.Server.Mobs
}
var mgr = IoCManager.Resolve<IPlayerManager>();
if (mgr.TryGetPlayerData(new NetSessionId(args[0]), out var data))
if (mgr.TryGetSessionByUsername(args[0], out var data))
{
var mind = data.ContentData().Mind;
var builder = new StringBuilder();
builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.SessionId, mind.OwnedMob?.Owner?.Uid);
builder.AppendFormat("player: {0}, mob: {1}\nroles: ", mind.UserId, mind.OwnedMob?.Owner?.Uid);
foreach (var role in mind.AllRoles)
{
builder.AppendFormat("{0} ", role.Name);
@@ -68,7 +68,7 @@ namespace Content.Server.Mobs
}
var mgr = IoCManager.Resolve<IPlayerManager>();
if (mgr.TryGetPlayerData(new NetSessionId(args[0]), out var data))
if (mgr.TryGetPlayerDataByUsername(args[0], out var data))
{
var mind = data.ContentData().Mind;
var role = new Job(mind, _prototypeManager.Index<JobPrototype>(args[1]));
@@ -100,7 +100,7 @@ namespace Content.Server.Mobs
}
var mgr = IoCManager.Resolve<IPlayerManager>();
if (mgr.TryGetPlayerData(new NetSessionId(args[0]), out var data))
if (mgr.TryGetPlayerDataByUsername(args[0], out var data))
{
var mind = data.ContentData().Mind;
var role = new Job(mind, _prototypeManager.Index<JobPrototype>(args[1]));

View File

@@ -30,10 +30,10 @@ namespace Content.Server.Mobs
/// <summary>
/// Creates the new mind attached to a specific player session.
/// </summary>
/// <param name="sessionId">The session ID of the owning player.</param>
public Mind(NetSessionId sessionId)
/// <param name="userId">The session ID of the owning player.</param>
public Mind(NetUserId userId)
{
SessionId = sessionId;
UserId = userId;
}
// TODO: This session should be able to be changed, probably.
@@ -41,7 +41,7 @@ namespace Content.Server.Mobs
/// The session ID of the player owning this mind.
/// </summary>
[ViewVariables]
public NetSessionId? SessionId { get; private set; }
public NetUserId? UserId { get; private set; }
[ViewVariables]
public bool IsVisitingEntity => VisitingEntity != null;
@@ -83,12 +83,12 @@ namespace Content.Server.Mobs
{
get
{
if (!SessionId.HasValue)
if (!UserId.HasValue)
{
return null;
}
var playerMgr = IoCManager.Resolve<IPlayerManager>();
playerMgr.TryGetSessionById(SessionId.Value, out var ret);
playerMgr.TryGetSessionById(UserId.Value, out var ret);
return ret;
}
}
@@ -195,7 +195,7 @@ namespace Content.Server.Mobs
VisitingEntity = null;
}
public void ChangeOwningPlayer(NetSessionId? newOwner)
public void ChangeOwningPlayer(NetUserId? newOwner)
{
var playerMgr = IoCManager.Resolve<IPlayerManager>();
PlayerData newOwnerData = null;
@@ -216,12 +216,12 @@ namespace Content.Server.Mobs
var oldSession = Session;
oldSession?.AttachToEntity(null);
if (SessionId.HasValue)
if (UserId.HasValue)
{
playerMgr.GetPlayerData(SessionId.Value).ContentData().Mind = null;
playerMgr.GetPlayerData(UserId.Value).ContentData().Mind = null;
}
SessionId = newOwner;
UserId = newOwner;
if (!newOwner.HasValue)
{
return;

View File

@@ -16,7 +16,7 @@ namespace Content.Server.Players
/// The session ID of the player owning this data.
/// </summary>
[ViewVariables]
public NetSessionId SessionId { get; }
public NetUserId UserId { get; }
/// <summary>
/// The currently occupied mind of the player owning this data.
@@ -31,9 +31,9 @@ namespace Content.Server.Players
Mind = null;
}
public PlayerData(NetSessionId sessionId)
public PlayerData(NetUserId userId)
{
SessionId = sessionId;
UserId = userId;
}
}

View File

@@ -1,167 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Shared.Preferences;
using Robust.Shared.Maths;
using static Content.Shared.Preferences.Sex;
namespace Content.Server.Preferences
{
/// <summary>
/// Provides methods to retrieve and update character preferences.
/// Don't use this directly, go through <see cref="ServerPreferencesManager" /> instead.
/// </summary>
public class PreferencesDatabase
{
private readonly int _maxCharacterSlots;
private readonly PrefsDb _prefsDb;
// We use a single DbContext for the entire DB connection, and EFCore doesn't allow concurrent access.
// So we need this semaphore to prevent bugs.
private readonly SemaphoreSlim _prefsSemaphore = new SemaphoreSlim(1, 1);
public PreferencesDatabase(IDatabaseConfiguration dbConfig, int maxCharacterSlots)
{
_maxCharacterSlots = maxCharacterSlots;
_prefsDb = new PrefsDb(dbConfig);
}
public async Task<PlayerPreferences> GetPlayerPreferencesAsync(string username)
{
await _prefsSemaphore.WaitAsync();
try
{
var prefs = await _prefsDb.GetPlayerPreferences(username);
if (prefs is null) return null;
var profiles = new ICharacterProfile[_maxCharacterSlots];
foreach (var profile in prefs.HumanoidProfiles)
{
profiles[profile.Slot] = ConvertProfiles(profile);
}
return new PlayerPreferences
(
profiles,
prefs.SelectedCharacterSlot
);
}
finally
{
_prefsSemaphore.Release();
}
}
public async Task SaveSelectedCharacterIndexAsync(string username, int index)
{
await _prefsSemaphore.WaitAsync();
try
{
index = MathHelper.Clamp(index, 0, _maxCharacterSlots - 1);
await _prefsDb.SaveSelectedCharacterIndex(username, index);
}
finally
{
_prefsSemaphore.Release();
}
}
public async Task SaveCharacterSlotAsync(string username, ICharacterProfile profile, int slot)
{
if (slot < 0 || slot >= _maxCharacterSlots)
return;
await _prefsSemaphore.WaitAsync();
try
{
if (profile is null)
{
await DeleteCharacterSlotAsync(username, slot);
return;
}
if (!(profile is HumanoidCharacterProfile humanoid))
// TODO: Handle other ICharacterProfile implementations properly
throw new NotImplementedException();
var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
var entity = new HumanoidProfile
{
SlotName = humanoid.Name,
CharacterName = humanoid.Name,
Age = humanoid.Age,
Sex = humanoid.Sex.ToString(),
HairName = appearance.HairStyleName,
HairColor = appearance.HairColor.ToHex(),
FacialHairName = appearance.FacialHairStyleName,
FacialHairColor = appearance.FacialHairColor.ToHex(),
EyeColor = appearance.EyeColor.ToHex(),
SkinColor = appearance.SkinColor.ToHex(),
Slot = slot,
PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable
};
entity.Jobs.AddRange(
humanoid.JobPriorities
.Where(j => j.Value != JobPriority.Never)
.Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
);
entity.Antags.AddRange(
humanoid.AntagPreferences
.Select(a => new Antag {AntagName = a})
);
await _prefsDb.SaveCharacterSlotAsync(username, entity);
}
finally
{
_prefsSemaphore.Release();
}
}
private async Task DeleteCharacterSlotAsync(string username, int slot)
{
await _prefsDb.DeleteCharacterSlotAsync(username, slot);
}
public async Task<IEnumerable<KeyValuePair<string, ICharacterProfile>>> GetSelectedProfilesForPlayersAsync(
List<string> usernames)
{
await _prefsSemaphore.WaitAsync();
try
{
var profiles = await _prefsDb.GetProfilesForPlayersAsync(usernames);
return profiles.Select(
p => new KeyValuePair<string, ICharacterProfile>(p.Key, ConvertProfiles(p.Value)));
}
finally
{
_prefsSemaphore.Release();
}
}
private static HumanoidCharacterProfile ConvertProfiles(HumanoidProfile profile)
{
var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority);
var antags = profile.Antags.Select(a => a.AntagName);
return new HumanoidCharacterProfile(
profile.CharacterName,
profile.Age,
profile.Sex == "Male" ? Male : Female,
new HumanoidCharacterAppearance
(
profile.HairName,
Color.FromHex(profile.HairColor),
profile.FacialHairName,
Color.FromHex(profile.FacialHairColor),
Color.FromHex(profile.EyeColor),
Color.FromHex(profile.SkinColor)
),
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToList()
);
}
}
}

View File

@@ -1,15 +1,20 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Database;
using Content.Server.Interfaces;
using Content.Shared;
using Content.Shared.Preferences;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Resources;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
#nullable enable
namespace Content.Server.Preferences
{
@@ -20,119 +25,188 @@ namespace Content.Server.Preferences
public class ServerPreferencesManager : SharedPreferencesManager, IServerPreferencesManager
{
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPrototypeManager _protos = default!;
private PreferencesDatabase _preferencesDb;
private Task<PreferencesDatabase> _prefsDbLoadTask;
// Cache player prefs on the server so we don't need as much async hell related to them.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
new Dictionary<NetUserId, PlayerPrefData>();
public void StartInit()
private int MaxCharacterSlots => _cfg.GetCVar(CCVars.GameMaxCharacterSlots);
public void Init()
{
_netManager.RegisterNetMessage<MsgPreferencesAndSettings>(nameof(MsgPreferencesAndSettings));
_netManager.RegisterNetMessage<MsgSelectCharacter>(nameof(MsgSelectCharacter),
HandleSelectCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateCharacter>(nameof(MsgUpdateCharacter),
HandleUpdateCharacterMessage);
_configuration.RegisterCVar("game.maxcharacterslots", 10);
_configuration.RegisterCVar("database.prefs_engine", "sqlite");
_configuration.RegisterCVar("database.prefs_sqlite_dbpath", "preferences.db");
_configuration.RegisterCVar("database.prefs_pg_host", "localhost");
_configuration.RegisterCVar("database.prefs_pg_port", 5432);
_configuration.RegisterCVar("database.prefs_pg_database", "ss14_prefs");
_configuration.RegisterCVar("database.prefs_pg_username", string.Empty);
_configuration.RegisterCVar("database.prefs_pg_password", string.Empty);
var engine = _configuration.GetCVar<string>("database.prefs_engine").ToLower();
IDatabaseConfiguration dbConfig;
switch (engine)
{
case "sqlite":
var configPreferencesDbPath = _configuration.GetCVar<string>("database.prefs_sqlite_dbpath");
var inMemory = _resourceManager.UserData.RootDir == null;
var finalPreferencesDbPath = inMemory ?
null :
Path.Combine(_resourceManager.UserData.RootDir, configPreferencesDbPath);
dbConfig = new SqliteConfiguration(finalPreferencesDbPath);
break;
case "postgres":
dbConfig = new PostgresConfiguration(
_configuration.GetCVar<string>("database.prefs_pg_host"),
_configuration.GetCVar<int>("database.prefs_pg_port"),
_configuration.GetCVar<string>("database.prefs_pg_database"),
_configuration.GetCVar<string>("database.prefs_pg_username"),
_configuration.GetCVar<string>("database.prefs_pg_password")
);
break;
default:
throw new NotImplementedException("Unknown database engine {engine}.");
}
var maxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots");
// Actually loading the preferences database takes a while,
// because EFCore has to initialize and run migrations.
// We load it in the thread pool here and then fetch the .Result in FinishInit.
// This means it'll run in parallel with other loading like prototypes & map load.
_prefsDbLoadTask = Task.Run(() => new PreferencesDatabase(dbConfig, maxCharacterSlots));
}
public void FinishInit()
{
_preferencesDb = _prefsDbLoadTask.Result;
}
private async void HandleSelectCharacterMessage(MsgSelectCharacter message)
{
await _preferencesDb.SaveSelectedCharacterIndexAsync(message.MsgChannel.SessionId.Username,
message.SelectedCharacterIndex);
var index = message.SelectedCharacterIndex;
var userId = message.MsgChannel.UserId;
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded.IsCompleted)
{
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
return;
}
if (index < 0 || index >= MaxCharacterSlots)
{
return;
}
var curPrefs = prefsData.Prefs!;
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, index);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
await _db.SaveSelectedCharacterIndexAsync(message.MsgChannel.UserId, message.SelectedCharacterIndex);
}
}
private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message)
{
await _preferencesDb.SaveCharacterSlotAsync(message.MsgChannel.SessionId.Username, message.Profile,
message.Slot);
var slot = message.Slot;
var profile = message.Profile;
var userId = message.MsgChannel.UserId;
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded.IsCompleted)
{
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
return;
}
if (slot < 0 || slot >= MaxCharacterSlots)
{
return;
}
var curPrefs = prefsData.Prefs!;
var arr = new ICharacterProfile[MaxCharacterSlots];
curPrefs.Characters.ToList().CopyTo(arr, 0);
arr[slot] = HumanoidCharacterProfile.EnsureValid((HumanoidCharacterProfile) profile, _protos);
prefsData.Prefs = new PlayerPreferences(arr, slot);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
await _db.SaveCharacterSlotAsync(message.MsgChannel.UserId, message.Profile, message.Slot);
}
}
public async void OnClientConnected(IPlayerSession session)
{
var msg = _netManager.CreateNetMessage<MsgPreferencesAndSettings>();
msg.Preferences = await GetPreferencesAsync(session.SessionId.Username);
msg.Settings = new GameSettings
if (!ShouldStorePrefs(session.ConnectedClient.AuthType))
{
MaxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots")
};
_netManager.ServerSendMessage(msg, session.ConnectedClient);
// Don't store data for guests.
var prefsData = new PlayerPrefData
{
PrefsLoaded = Task.CompletedTask,
Prefs = new PlayerPreferences(
new ICharacterProfile[] {HumanoidCharacterProfile.Default()},
0)
};
_cachedPlayerPrefs[session.UserId] = prefsData;
}
else
{
var prefsData = new PlayerPrefData();
var loadTask = LoadPrefs();
prefsData.PrefsLoaded = loadTask;
_cachedPlayerPrefs[session.UserId] = prefsData;
await loadTask;
async Task LoadPrefs()
{
var prefs = await GetOrCreatePreferencesAsync(session.UserId);
prefsData.Prefs = prefs;
var msg = _netManager.CreateNetMessage<MsgPreferencesAndSettings>();
msg.Preferences = prefs;
msg.Settings = new GameSettings
{
MaxCharacterSlots = MaxCharacterSlots
};
_netManager.ServerSendMessage(msg, session.ConnectedClient);
}
}
}
/// <summary>
/// Returns the requested <see cref="PlayerPreferences"/> or null if not found.
/// </summary>
private async Task<PlayerPreferences> GetFromSql(string username)
public void OnClientDisconnected(IPlayerSession session)
{
return await _preferencesDb.GetPlayerPreferencesAsync(username);
_cachedPlayerPrefs.Remove(session.UserId);
}
public bool HavePreferencesLoaded(IPlayerSession session)
{
return _cachedPlayerPrefs.ContainsKey(session.UserId);
}
public Task WaitPreferencesLoaded(IPlayerSession session)
{
return _cachedPlayerPrefs[session.UserId].PrefsLoaded;
}
/// <summary>
/// Retrieves preferences for the given username from storage.
/// Creates and saves default preferences if they are not found, then returns them.
/// </summary>
public async Task<PlayerPreferences> GetPreferencesAsync(string username)
public PlayerPreferences GetPreferences(NetUserId userId)
{
var prefs = await GetFromSql(username);
if (prefs is null)
var prefs = _cachedPlayerPrefs[userId].Prefs;
if (prefs == null)
{
await _preferencesDb.SaveSelectedCharacterIndexAsync(username, 0);
await _preferencesDb.SaveCharacterSlotAsync(username, HumanoidCharacterProfile.Default(), 0);
prefs = await GetFromSql(username);
throw new InvalidOperationException("Preferences for this player have not loaded yet.");
}
return prefs;
}
public async Task<IEnumerable<KeyValuePair<string, ICharacterProfile>>> GetSelectedProfilesForPlayersAsync(List<string> usernames)
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
{
return await _preferencesDb.GetSelectedProfilesForPlayersAsync(usernames);
var prefs = await _db.GetPlayerPreferencesAsync(userId);
if (prefs is null)
{
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Default());
}
return prefs;
}
public IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(
List<NetUserId> usernames)
{
return usernames
.Select(p => (_cachedPlayerPrefs[p].Prefs, p))
.Where(p => p.Prefs != null)
.Select(p =>
{
var idx = p.Prefs!.SelectedCharacterIndex;
return new KeyValuePair<NetUserId, ICharacterProfile>(p.p, p.Prefs!.GetProfile(idx));
});
}
internal static bool ShouldStorePrefs(LoginType loginType)
{
return loginType.HasStaticUserId();
}
private sealed class PlayerPrefData
{
public Task PrefsLoaded = default!;
public PlayerPreferences? Prefs;
}
}
}

View File

@@ -3,6 +3,7 @@ using Content.Server.AI.WorldState;
using Content.Server.Body.Network;
using Content.Server.Cargo;
using Content.Server.Chat;
using Content.Server.Database;
using Content.Server.GameObjects.Components.Mobs.Speech;
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
using Content.Server.GameObjects.Components.Power.PowerNetComponents;
@@ -34,6 +35,7 @@ namespace Content.Server
IoCManager.Register<ICargoOrderDataManager, CargoOrderDataManager>();
IoCManager.Register<IModuleManager, ServerModuleManager>();
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
IoCManager.Register<IServerDbManager, ServerDbManager>();
IoCManager.Register<RecipeManager, RecipeManager>();
IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>();
IoCManager.Register<INodeGroupFactory, NodeGroupFactory>();
@@ -43,6 +45,7 @@ namespace Content.Server
IoCManager.Register<ConsiderationsManager, ConsiderationsManager>();
IoCManager.Register<IBodyNetworkFactory, BodyNetworkFactory>();
IoCManager.Register<IAccentManager, AccentManager>();
IoCManager.Register<IConnectionManager, ConnectionManager>();
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Content.Server.Utility
{
public static class IPAddressExt
{
// Taken from https://stackoverflow.com/a/56461160/4678631
public static bool IsInSubnet(this IPAddress address, string subnetMask)
{
var slashIdx = subnetMask.IndexOf("/", StringComparison.Ordinal);
if (slashIdx == -1)
{
// We only handle netmasks in format "IP/PrefixLength".
throw new NotSupportedException("Only SubNetMasks with a given prefix length are supported.");
}
// First parse the address of the netmask before the prefix length.
var maskAddress = IPAddress.Parse(subnetMask.Substring(0, slashIdx));
if (maskAddress.AddressFamily != address.AddressFamily)
{
// We got something like an IPV4-Address for an IPv6-Mask. This is not valid.
return false;
}
// Now find out how long the prefix is.
int maskLength = int.Parse(subnetMask.Substring(slashIdx + 1));
if (maskAddress.AddressFamily == AddressFamily.InterNetwork)
{
// Convert the mask address to an unsigned integer.
var maskAddressBits = BitConverter.ToUInt32(maskAddress.GetAddressBytes().Reverse().ToArray(), 0);
// And convert the IpAddress to an unsigned integer.
var ipAddressBits = BitConverter.ToUInt32(address.GetAddressBytes().Reverse().ToArray(), 0);
// Get the mask/network address as unsigned integer.
uint mask = uint.MaxValue << (32 - maskLength);
// https://stackoverflow.com/a/1499284/3085985
// Bitwise AND mask and MaskAddress, this should be the same as mask and IpAddress
// as the end of the mask is 0000 which leads to both addresses to end with 0000
// and to start with the prefix.
return (maskAddressBits & mask) == (ipAddressBits & mask);
}
if (maskAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
// Convert the mask address to a BitArray.
var maskAddressBits = new BitArray(maskAddress.GetAddressBytes());
// And convert the IpAddress to a BitArray.
var ipAddressBits = new BitArray(address.GetAddressBytes());
if (maskAddressBits.Length != ipAddressBits.Length)
{
throw new ArgumentException("Length of IP Address and Subnet Mask do not match.");
}
// Compare the prefix bits.
for (int maskIndex = 0; maskIndex < maskLength; maskIndex++)
{
if (ipAddressBits[maskIndex] != maskAddressBits[maskIndex])
{
return false;
}
}
return true;
}
throw new NotSupportedException("Only InterNetworkV6 or InterNetwork address families are supported.");
}
}
}