Play time tracking: Job timers 3: more titles: when the (#9978)
Co-authored-by: Veritius <veritiusgaming@gmail.com> Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
committed by
GitHub
parent
6b94db0336
commit
e852ada6c8
336
Content.Server/Administration/Commands/PlayTimeCommands.cs
Normal file
336
Content.Server/Administration/Commands/PlayTimeCommands.cs
Normal file
@@ -0,0 +1,336 @@
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class PlayTimeAddOverallCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
public string Command => "playtime_addoverall";
|
||||
public string Description => Loc.GetString("cmd-playtime_addoverall-desc");
|
||||
public string Help => Loc.GetString("cmd-playtime_addoverall-help", ("command", Command));
|
||||
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playtime_addoverall-error-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[1], out var minutes))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parse-minutes-fail", ("minutes", args[1])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playerManager.TryGetSessionByUsername(args[0], out var player))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parse-session-fail", ("username", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
_playTimeTracking.AddTimeToOverallPlaytime(player, TimeSpan.FromMinutes(minutes));
|
||||
var overall = _playTimeTracking.GetOverallPlaytime(player);
|
||||
|
||||
shell.WriteLine(Loc.GetString(
|
||||
"cmd-playtime_addoverall-succeed",
|
||||
("username", args[0]),
|
||||
("time", overall)));
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.SessionNames(),
|
||||
Loc.GetString("cmd-playtime_addoverall-arg-user"));
|
||||
|
||||
if (args.Length == 2)
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-playtime_addoverall-arg-minutes"));
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PlayTimeAddRoleCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
public string Command => "playtime_addrole";
|
||||
public string Description => Loc.GetString("cmd-playtime_addrole-desc");
|
||||
public string Help => Loc.GetString("cmd-playtime_addrole-help", ("command", Command));
|
||||
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playtime_addrole-error-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
var userName = args[0];
|
||||
if (!_playerManager.TryGetSessionByUsername(userName, out var player))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parse-session-fail", ("username", userName)));
|
||||
return;
|
||||
}
|
||||
|
||||
var role = args[1];
|
||||
|
||||
var m = args[2];
|
||||
if (!int.TryParse(m, out var minutes))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parse-minutes-fail", ("minutes", minutes)));
|
||||
return;
|
||||
}
|
||||
|
||||
_playTimeTracking.AddTimeToTracker(player, role, TimeSpan.FromMinutes(minutes));
|
||||
var time = _playTimeTracking.GetOverallPlaytime(player);
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_addrole-succeed",
|
||||
("username", userName),
|
||||
("role", role),
|
||||
("time", time)));
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-playtime_addrole-arg-user"));
|
||||
}
|
||||
|
||||
if (args.Length == 2)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.PrototypeIDs<PlayTimeTrackerPrototype>(),
|
||||
Loc.GetString("cmd-playtime_addrole-arg-role"));
|
||||
}
|
||||
|
||||
if (args.Length == 3)
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-playtime_addrole-arg-minutes"));
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class PlayTimeGetOverallCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
public string Command => "playtime_getoverall";
|
||||
public string Description => Loc.GetString("cmd-playtime_getoverall-desc");
|
||||
public string Help => Loc.GetString("cmd-playtime_getoverall-help", ("command", Command));
|
||||
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playtime_getoverall-error-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
var userName = args[0];
|
||||
if (!_playerManager.TryGetSessionByUsername(userName, out var player))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parser-session-fail", ("username", userName)));
|
||||
return;
|
||||
}
|
||||
|
||||
var value = _playTimeTracking.GetOverallPlaytime(player);
|
||||
shell.WriteLine(Loc.GetString(
|
||||
"cmd-playtime_getoverall-success",
|
||||
("username", userName),
|
||||
("time", value)));
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-playtime_getoverall-arg-user"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class PlayTimeGetRoleCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
public string Command => "playtime_getrole";
|
||||
public string Description => Loc.GetString("cmd-playtime_getrole-desc");
|
||||
public string Help => Loc.GetString("cmd-playtime_getrole-help", ("command", Command));
|
||||
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is not (1 or 2))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_getrole-error-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
var userName = args[0];
|
||||
if (!_playerManager.TryGetSessionByUsername(userName, out var session))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parser-session-fail", ("username", userName)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var timers = _playTimeTracking.GetTrackerTimes(session);
|
||||
|
||||
if (timers.Count == 0)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_getrole-no"));
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (role, time) in timers)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_getrole-role", ("role", role), ("time", time)));
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Length >= 2)
|
||||
{
|
||||
if (args[1] == "Overall")
|
||||
{
|
||||
var timer = _playTimeTracking.GetOverallPlaytime(session);
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_getrole-overall", ("time", timer)));
|
||||
return;
|
||||
}
|
||||
|
||||
var time = _playTimeTracking.GetPlayTimeForTracker(session, args[1]);
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_getrole-succeed", ("username", session.Name),
|
||||
("time", time)));
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-playtime_getrole-arg-user"));
|
||||
}
|
||||
|
||||
if (args.Length == 2)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.PrototypeIDs<PlayTimeTrackerPrototype>(),
|
||||
Loc.GetString("cmd-playtime_getrole-arg-role"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the timers for a particular player immediately
|
||||
/// </summary>
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class PlayTimeSaveCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
public string Command => "playtime_save";
|
||||
public string Description => Loc.GetString("cmd-playtime_save-desc");
|
||||
public string Help => Loc.GetString("cmd-playtime_save-help", ("command", Command));
|
||||
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_save-error-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
if (!_playerManager.TryGetSessionByUsername(name, out var pSession))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parse-session-fail", ("username", name)));
|
||||
return;
|
||||
}
|
||||
|
||||
_playTimeTracking.SaveSession(pSession);
|
||||
shell.WriteLine(Loc.GetString("cmd-playtime_save-succeed", ("username", name)));
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-playtime_save-arg-user"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class PlayTimeFlushCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
public string Command => "playtime_flush";
|
||||
public string Description => Loc.GetString("cmd-playtime_flush-desc");
|
||||
public string Help => Loc.GetString("cmd-playtime_flush-help", ("command", Command));
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is not (0 or 1))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playtime_flush-error-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
_playTimeTracking.FlushAllTrackers();
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
if (!_playerManager.TryGetSessionByUsername(name, out var pSession))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("parse-session-fail", ("username", name)));
|
||||
return;
|
||||
}
|
||||
|
||||
_playTimeTracking.FlushTracker(pSession);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-playtime_flush-arg-user"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
96
Content.Server/Afk/AFKSystem.cs
Normal file
96
Content.Server/Afk/AFKSystem.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Afk.Events;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Afk;
|
||||
|
||||
/// <summary>
|
||||
/// Actively checks for AFK players regularly and issues an event whenever they go afk.
|
||||
/// </summary>
|
||||
public sealed class AFKSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAfkManager _afkManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly GameTicker _ticker = default!;
|
||||
|
||||
private float _checkDelay;
|
||||
private float _accumulator;
|
||||
|
||||
private readonly HashSet<IPlayerSession> _afkPlayers = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_playerManager.PlayerStatusChanged += OnPlayerChange;
|
||||
_configManager.OnValueChanged(CCVars.AfkTime, SetAfkDelay, true);
|
||||
}
|
||||
|
||||
private void SetAfkDelay(float obj)
|
||||
{
|
||||
_checkDelay = obj;
|
||||
}
|
||||
|
||||
private void OnPlayerChange(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
switch (e.NewStatus)
|
||||
{
|
||||
case SessionStatus.Disconnected:
|
||||
_afkPlayers.Remove(e.Session);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_afkPlayers.Clear();
|
||||
_accumulator = 0f;
|
||||
_playerManager.PlayerStatusChanged -= OnPlayerChange;
|
||||
_configManager.UnsubValueChanged(CCVars.AfkTime, SetAfkDelay);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (_ticker.RunLevel != GameRunLevel.InRound)
|
||||
{
|
||||
_afkPlayers.Clear();
|
||||
_accumulator = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulator += frameTime;
|
||||
|
||||
// TODO: Should also listen to the input events for more accurate timings.
|
||||
if (_accumulator < _checkDelay) return;
|
||||
|
||||
_accumulator -= _checkDelay;
|
||||
|
||||
foreach (var session in Filter.GetAllPlayers())
|
||||
{
|
||||
var pSession = (IPlayerSession) session;
|
||||
var isAfk = _afkManager.IsAfk(pSession);
|
||||
|
||||
if (isAfk && _afkPlayers.Add(pSession))
|
||||
{
|
||||
var ev = new AFKEvent(pSession);
|
||||
RaiseLocalEvent(ref ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isAfk && _afkPlayers.Remove(pSession))
|
||||
{
|
||||
var ev = new UnAFKEvent(pSession);
|
||||
RaiseLocalEvent(ref ev);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Content.Server/Afk/Events/AFKEvent.cs
Normal file
17
Content.Server/Afk/Events/AFKEvent.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Robust.Server.Player;
|
||||
|
||||
namespace Content.Server.Afk.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever a player goes afk.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly struct AFKEvent
|
||||
{
|
||||
public readonly IPlayerSession Session;
|
||||
|
||||
public AFKEvent(IPlayerSession playerSession)
|
||||
{
|
||||
Session = playerSession;
|
||||
}
|
||||
}
|
||||
17
Content.Server/Afk/Events/UnAFKEvent.cs
Normal file
17
Content.Server/Afk/Events/UnAFKEvent.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Robust.Server.Player;
|
||||
|
||||
namespace Content.Server.Afk.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever a player is no longer AFK.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly struct UnAFKEvent
|
||||
{
|
||||
public readonly IPlayerSession Session;
|
||||
|
||||
public UnAFKEvent(IPlayerSession playerSession)
|
||||
{
|
||||
Session = playerSession;
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@ namespace Content.Server.Afk
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class IsAfkCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
|
||||
public string Command => "isafk";
|
||||
public string Description => "Checks if a specified player is AFK";
|
||||
public string Help => "Usage: isafk <playerName>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
var afkManager = IoCManager.Resolve<IAfkManager>();
|
||||
|
||||
if (args.Length == 0)
|
||||
@@ -23,7 +24,7 @@ namespace Content.Server.Afk
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playerManager.TryGetSessionByUsername(args[0], out var player))
|
||||
if (!_players.TryGetSessionByUsername(args[0], out var player))
|
||||
{
|
||||
shell.WriteError("Unable to find that player");
|
||||
return;
|
||||
@@ -31,5 +32,17 @@ namespace Content.Server.Afk
|
||||
|
||||
shell.WriteLine(afkManager.IsAfk(player) ? "They are indeed AFK" : "They are not AFK");
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _players),
|
||||
"<playerName>");
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +359,59 @@ namespace Content.Server.Database
|
||||
public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
|
||||
#endregion
|
||||
|
||||
#region Playtime
|
||||
public async Task<List<PlayTime>> GetPlayTimes(Guid player)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
return await db.DbContext.PlayTime
|
||||
.Where(p => p.PlayerId == player)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
// Ideally I would just be able to send a bunch of UPSERT commands, but EFCore is a pile of garbage.
|
||||
// So... In the interest of not making this take forever at high update counts...
|
||||
// Bulk-load play time objects for all players involved.
|
||||
// This allows us to semi-efficiently load all entities we need in a single DB query.
|
||||
// Then we can update & insert without further round-trips to the DB.
|
||||
|
||||
var players = updates.Select(u => u.User.UserId).Distinct().ToArray();
|
||||
var dbTimes = (await db.DbContext.PlayTime
|
||||
.Where(p => players.Contains(p.PlayerId))
|
||||
.ToArrayAsync())
|
||||
.GroupBy(p => p.PlayerId)
|
||||
.ToDictionary(g => g.Key, g => g.ToDictionary(p => p.Tracker, p => p));
|
||||
|
||||
foreach (var (user, tracker, time) in updates)
|
||||
{
|
||||
if (dbTimes.TryGetValue(user.UserId, out var userTimes)
|
||||
&& userTimes.TryGetValue(tracker, out var ent))
|
||||
{
|
||||
// Already have a tracker in the database, update it.
|
||||
ent.TimeSpent = time;
|
||||
continue;
|
||||
}
|
||||
|
||||
// No tracker, make a new one.
|
||||
var playTime = new PlayTime
|
||||
{
|
||||
Tracker = tracker,
|
||||
PlayerId = user.UserId,
|
||||
TimeSpent = time
|
||||
};
|
||||
|
||||
db.DbContext.PlayTime.Add(playTime);
|
||||
}
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Player Records
|
||||
/*
|
||||
* PLAYER RECORDS
|
||||
@@ -597,14 +650,15 @@ namespace Content.Server.Database
|
||||
|
||||
#region Admin Logs
|
||||
|
||||
public async Task<Server> AddOrGetServer(string serverName)
|
||||
public async Task<(Server, bool existed)> AddOrGetServer(string serverName)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
var server = await db.DbContext.Server.Where(server => server.Name.Equals(serverName)).SingleOrDefaultAsync();
|
||||
var server = await db.DbContext.Server
|
||||
.Where(server => server.Name.Equals(serverName))
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (server != default)
|
||||
{
|
||||
return server;
|
||||
}
|
||||
return (server, true);
|
||||
|
||||
server = new Server
|
||||
{
|
||||
@@ -615,7 +669,7 @@ namespace Content.Server.Database
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
|
||||
return server;
|
||||
return (server, false);
|
||||
}
|
||||
|
||||
public virtual async Task AddAdminLogs(List<QueuedLog> logs)
|
||||
@@ -921,5 +975,6 @@ namespace Content.Server.Database
|
||||
|
||||
public abstract ValueTask DisposeAsync();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using Prometheus;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
@@ -114,6 +115,23 @@ namespace Content.Server.Database
|
||||
Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
|
||||
#endregion
|
||||
|
||||
#region Playtime
|
||||
|
||||
/// <summary>
|
||||
/// Look up a player's role timers.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to get the role timer information from.</param>
|
||||
/// <returns>All role timers belonging to the player.</returns>
|
||||
Task<List<PlayTime>> GetPlayTimes(Guid player);
|
||||
|
||||
/// <summary>
|
||||
/// Update play time information in bulk.
|
||||
/// </summary>
|
||||
/// <param name="updates">The list of all updates to apply to the database.</param>
|
||||
Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Player Records
|
||||
Task UpdatePlayerRecordAsync(
|
||||
NetUserId userId,
|
||||
@@ -209,6 +227,14 @@ namespace Content.Server.Database
|
||||
|
||||
public sealed class ServerDbManager : IServerDbManager
|
||||
{
|
||||
public static readonly Counter DbReadOpsMetric = Metrics.CreateCounter(
|
||||
"db_read_ops",
|
||||
"Amount of read operations processed by the database manager.");
|
||||
|
||||
public static readonly Counter DbWriteOpsMetric = Metrics.CreateCounter(
|
||||
"db_write_ops",
|
||||
"Amount of write operations processed by the database manager.");
|
||||
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly ILogManager _logMgr = default!;
|
||||
@@ -244,46 +270,55 @@ namespace Content.Server.Database
|
||||
|
||||
public Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.InitPrefsAsync(userId, defaultProfile);
|
||||
}
|
||||
|
||||
public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.SaveSelectedCharacterIndexAsync(userId, index);
|
||||
}
|
||||
|
||||
public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.SaveCharacterSlotAsync(userId, profile, slot);
|
||||
}
|
||||
|
||||
public Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.DeleteSlotAndSetSelectedIndex(userId, deleteSlot, newSlot);
|
||||
}
|
||||
|
||||
public Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.SaveAdminOOCColorAsync(userId, color);
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetPlayerPreferencesAsync(userId);
|
||||
}
|
||||
|
||||
public Task AssignUserIdAsync(string name, NetUserId userId)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AssignUserIdAsync(name, userId);
|
||||
}
|
||||
|
||||
public Task<NetUserId?> GetAssignedUserIdAsync(string name)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAssignedUserIdAsync(name);
|
||||
}
|
||||
|
||||
public Task<ServerBanDef?> GetServerBanAsync(int id)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetServerBanAsync(id);
|
||||
}
|
||||
|
||||
@@ -292,6 +327,7 @@ namespace Content.Server.Database
|
||||
NetUserId? userId,
|
||||
ImmutableArray<byte>? hwId)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetServerBanAsync(address, userId, hwId);
|
||||
}
|
||||
|
||||
@@ -301,22 +337,26 @@ namespace Content.Server.Database
|
||||
ImmutableArray<byte>? hwId,
|
||||
bool includeUnbanned=true)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetServerBansAsync(address, userId, hwId, includeUnbanned);
|
||||
}
|
||||
|
||||
public Task AddServerBanAsync(ServerBanDef serverBan)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddServerBanAsync(serverBan);
|
||||
}
|
||||
|
||||
public Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddServerUnbanAsync(serverUnban);
|
||||
}
|
||||
|
||||
#region Role Ban
|
||||
public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetServerRoleBanAsync(id);
|
||||
}
|
||||
|
||||
@@ -326,36 +366,58 @@ namespace Content.Server.Database
|
||||
ImmutableArray<byte>? hwId,
|
||||
bool includeUnbanned = true)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned);
|
||||
}
|
||||
|
||||
public Task AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddServerRoleBanAsync(serverRoleBan);
|
||||
}
|
||||
|
||||
public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddServerRoleUnbanAsync(serverRoleUnban);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Playtime
|
||||
|
||||
public Task<List<PlayTime>> GetPlayTimes(Guid player)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetPlayTimes(player);
|
||||
}
|
||||
|
||||
public Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.UpdatePlayTimes(updates);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public Task UpdatePlayerRecordAsync(
|
||||
NetUserId userId,
|
||||
string userName,
|
||||
IPAddress address,
|
||||
ImmutableArray<byte> hwId)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.UpdatePlayerRecord(userId, userName, address, hwId);
|
||||
}
|
||||
|
||||
public Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetPlayerRecordByUserName(userName, cancel);
|
||||
}
|
||||
|
||||
public Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetPlayerRecordByUserId(userId, cancel);
|
||||
}
|
||||
|
||||
@@ -366,137 +428,169 @@ namespace Content.Server.Database
|
||||
ImmutableArray<byte> hwId,
|
||||
ConnectionDenyReason? denied)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddConnectionLogAsync(userId, userName, address, hwId, denied);
|
||||
}
|
||||
|
||||
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddServerBanHitsAsync(connection, bans);
|
||||
}
|
||||
|
||||
public Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminDataForAsync(userId, cancel);
|
||||
}
|
||||
|
||||
public Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminRankDataForAsync(id, cancel);
|
||||
}
|
||||
|
||||
public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAllAdminAndRanksAsync(cancel);
|
||||
}
|
||||
|
||||
public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.RemoveAdminAsync(userId, cancel);
|
||||
}
|
||||
|
||||
public Task AddAdminAsync(Admin admin, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddAdminAsync(admin, cancel);
|
||||
}
|
||||
|
||||
public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.UpdateAdminAsync(admin, cancel);
|
||||
}
|
||||
|
||||
public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.RemoveAdminRankAsync(rankId, cancel);
|
||||
}
|
||||
|
||||
public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddAdminRankAsync(rank, cancel);
|
||||
}
|
||||
|
||||
public Task<int> AddNewRound(Server server, params Guid[] playerIds)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddNewRound(server, playerIds);
|
||||
}
|
||||
|
||||
public Task<Round> GetRound(int id)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetRound(id);
|
||||
}
|
||||
|
||||
public Task AddRoundPlayers(int id, params Guid[] playerIds)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddRoundPlayers(id, playerIds);
|
||||
}
|
||||
|
||||
public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.UpdateAdminRankAsync(rank, cancel);
|
||||
}
|
||||
|
||||
public Task<Server> AddOrGetServer(string serverName)
|
||||
public async Task<Server> AddOrGetServer(string serverName)
|
||||
{
|
||||
return _db.AddOrGetServer(serverName);
|
||||
var (server, existed) = await _db.AddOrGetServer(serverName);
|
||||
if (existed)
|
||||
DbReadOpsMetric.Inc();
|
||||
else
|
||||
DbWriteOpsMetric.Inc();
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
public Task AddAdminLogs(List<QueuedLog> logs)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddAdminLogs(logs);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminLogMessages(filter);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminLogs(filter);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminLogsJson(filter);
|
||||
}
|
||||
|
||||
public Task<bool> GetWhitelistStatusAsync(NetUserId player)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetWhitelistStatusAsync(player);
|
||||
}
|
||||
|
||||
public Task AddToWhitelistAsync(NetUserId player)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddToWhitelistAsync(player);
|
||||
}
|
||||
|
||||
public Task RemoveFromWhitelistAsync(NetUserId player)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.RemoveFromWhitelistAsync(player);
|
||||
}
|
||||
|
||||
public Task AddUploadedResourceLogAsync(NetUserId user, DateTime date, string path, byte[] data)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.AddUploadedResourceLogAsync(user, date, path, data);
|
||||
}
|
||||
|
||||
public Task PurgeUploadedResourceLogAsync(int days)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.PurgeUploadedResourceLogAsync(days);
|
||||
}
|
||||
|
||||
public Task<DateTime?> GetLastReadRules(NetUserId player)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetLastReadRules(player);
|
||||
}
|
||||
|
||||
public Task SetLastReadRules(NetUserId player, DateTime time)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.SetLastReadRules(player, time);
|
||||
}
|
||||
|
||||
public Task<int> AddAdminNote(int? roundId, Guid player, string message, Guid createdBy, DateTime createdAt)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
var note = new AdminNote
|
||||
{
|
||||
RoundId = roundId,
|
||||
@@ -513,21 +607,25 @@ namespace Content.Server.Database
|
||||
|
||||
public Task<AdminNote?> GetAdminNote(int id)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminNote(id);
|
||||
}
|
||||
|
||||
public Task<List<AdminNote>> GetAdminNotes(Guid player)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return _db.GetAdminNotes(player);
|
||||
}
|
||||
|
||||
public Task DeleteAdminNote(int id, Guid deletedBy, DateTime deletedAt)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.DeleteAdminNote(id, deletedBy, deletedAt);
|
||||
}
|
||||
|
||||
public Task EditAdminNote(int id, string message, Guid editedBy, DateTime editedAt)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return _db.EditAdminNote(id, message, editedBy, editedAt);
|
||||
}
|
||||
|
||||
@@ -648,4 +746,6 @@ namespace Content.Server.Database
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record PlayTimeUpdate(NetUserId User, string Tracker, TimeSpan Time);
|
||||
}
|
||||
|
||||
70
Content.Server/Database/UserDbDataManager.cs
Normal file
70
Content.Server/Database/UserDbDataManager.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Manages per-user data that comes from the database. Ensures it is loaded efficiently on client connect,
|
||||
/// and ensures data is loaded before allowing players to spawn or such.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Actual loading code is handled by separate managers such as <see cref="IServerPreferencesManager"/>.
|
||||
/// This manager is simply a centralized "is loading done" controller for other code to rely on.
|
||||
/// </remarks>
|
||||
public sealed class UserDbDataManager
|
||||
{
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
private readonly Dictionary<NetUserId, UserData> _users = new();
|
||||
|
||||
// TODO: Ideally connected/disconnected would be subscribed to IPlayerManager directly,
|
||||
// but this runs into ordering issues with game ticker.
|
||||
public void ClientConnected(IPlayerSession session)
|
||||
{
|
||||
DebugTools.Assert(!_users.ContainsKey(session.UserId), "We should not have any cached data on client connect.");
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var task = Load(session, cts.Token);
|
||||
var data = new UserData(cts, task);
|
||||
|
||||
_users.Add(session.UserId, data);
|
||||
}
|
||||
|
||||
public void ClientDisconnected(IPlayerSession session)
|
||||
{
|
||||
_users.Remove(session.UserId, out var data);
|
||||
if (data == null)
|
||||
throw new InvalidOperationException("Did not have cached data in ClientDisconnect!");
|
||||
|
||||
data.Cancel.Cancel();
|
||||
data.Cancel.Dispose();
|
||||
|
||||
_prefs.OnClientDisconnected(session);
|
||||
_playTimeTracking.ClientDisconnected(session);
|
||||
}
|
||||
|
||||
private async Task Load(IPlayerSession session, CancellationToken cancel)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_prefs.LoadData(session, cancel),
|
||||
_playTimeTracking.LoadData(session, cancel));
|
||||
}
|
||||
|
||||
public Task WaitLoadComplete(IPlayerSession session)
|
||||
{
|
||||
return _users[session.UserId].Task;
|
||||
}
|
||||
|
||||
public bool IsLoadComplete(IPlayerSession session)
|
||||
{
|
||||
return _users[session.UserId].Task.IsCompleted;
|
||||
}
|
||||
|
||||
private sealed record UserData(CancellationTokenSource Cancel, Task Task);
|
||||
}
|
||||
@@ -14,9 +14,9 @@ using Content.Server.GhostKick;
|
||||
using Content.Server.GuideGenerator;
|
||||
using Content.Server.Info;
|
||||
using Content.Server.IoC;
|
||||
using Content.Server.LandMines;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerUpdates;
|
||||
using Content.Server.Voting.Managers;
|
||||
@@ -25,7 +25,6 @@ using Content.Shared.CCVar;
|
||||
using Content.Shared.Kitchen;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Bql;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Server.ServerStatus;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -40,6 +39,7 @@ namespace Content.Server.Entry
|
||||
private EuiManager _euiManager = default!;
|
||||
private IVoteManager _voteManager = default!;
|
||||
private ServerUpdateManager _updateManager = default!;
|
||||
private PlayTimeTrackingManager? _playTimeTracking;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init()
|
||||
@@ -78,8 +78,7 @@ namespace Content.Server.Entry
|
||||
_euiManager = IoCManager.Resolve<EuiManager>();
|
||||
_voteManager = IoCManager.Resolve<IVoteManager>();
|
||||
_updateManager = IoCManager.Resolve<ServerUpdateManager>();
|
||||
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
_playTimeTracking = IoCManager.Resolve<PlayTimeTrackingManager>();
|
||||
|
||||
var logManager = IoCManager.Resolve<ILogManager>();
|
||||
logManager.GetSawmill("Storage").Level = LogLevel.Info;
|
||||
@@ -96,6 +95,7 @@ namespace Content.Server.Entry
|
||||
|
||||
_voteManager.Initialize();
|
||||
_updateManager.Initialize();
|
||||
_playTimeTracking.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +151,14 @@ namespace Content.Server.Entry
|
||||
|
||||
case ModUpdateLevel.FramePostEngine:
|
||||
_updateManager.Update();
|
||||
_playTimeTracking?.Update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_playTimeTracking?.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +123,11 @@ namespace Content.Server.GameTicking
|
||||
|
||||
public void ToggleReady(IPlayerSession player, bool ready)
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player)) return;
|
||||
|
||||
if (!_prefsManager.HavePreferencesLoaded(player))
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player))
|
||||
return;
|
||||
|
||||
if (!_userDb.IsLoadComplete(player))
|
||||
return;
|
||||
}
|
||||
|
||||
var status = ready ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady;
|
||||
_playersInLobby[player] = ready ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady;
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
case SessionStatus.InGame:
|
||||
{
|
||||
_prefsManager.OnClientConnected(session);
|
||||
_userDb.ClientConnected(session);
|
||||
|
||||
var data = session.ContentData();
|
||||
|
||||
@@ -66,13 +66,13 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
|
||||
|
||||
SpawnWaitPrefs();
|
||||
SpawnWaitDb();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data.Mind.CurrentEntity == null)
|
||||
{
|
||||
SpawnWaitPrefs();
|
||||
SpawnWaitDb();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -90,16 +90,16 @@ namespace Content.Server.GameTicking
|
||||
|
||||
_chatManager.SendAdminAnnouncement(Loc.GetString("player-leave-message", ("name", args.Session.Name)));
|
||||
|
||||
_prefsManager.OnClientDisconnected(session);
|
||||
_userDb.ClientDisconnected(session);
|
||||
break;
|
||||
}
|
||||
}
|
||||
//When the status of a player changes, update the server info text
|
||||
UpdateInfoText();
|
||||
|
||||
async void SpawnWaitPrefs()
|
||||
async void SpawnWaitDb()
|
||||
{
|
||||
await _prefsManager.WaitPreferencesLoaded(session);
|
||||
await _userDb.WaitLoadComplete(session);
|
||||
SpawnPlayer(session, EntityUid.Invalid);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,13 @@ namespace Content.Server.GameTicking
|
||||
readyPlayers = _playersInLobby.Keys.ToList();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
foreach (var player in readyPlayers)
|
||||
{
|
||||
DebugTools.Assert(_userDb.IsLoadComplete(player), $"Player was readied up but didn't have user DB data loaded yet??");
|
||||
}
|
||||
#endif
|
||||
|
||||
readyPlayers.RemoveAll(p =>
|
||||
{
|
||||
if (_roleBanManager.GetRoleBans(p.UserId) != null)
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Linq;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
@@ -18,6 +17,7 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Job = Content.Server.Roles.Job;
|
||||
|
||||
namespace Content.Server.GameTicking
|
||||
{
|
||||
@@ -92,7 +92,10 @@ namespace Content.Server.GameTicking
|
||||
var character = GetPlayerProfile(player);
|
||||
|
||||
var jobBans = _roleBanManager.GetJobBans(player.UserId);
|
||||
if (jobBans == null || (jobId != null && jobBans.Contains(jobId)))
|
||||
if (jobBans == null || jobId != null && jobBans.Contains(jobId))
|
||||
return;
|
||||
|
||||
if (jobId != null && !_playTimeTrackings.IsAllowed(player, jobId))
|
||||
return;
|
||||
SpawnPlayer(player, character, station, jobId, lateJoin);
|
||||
}
|
||||
@@ -130,9 +133,18 @@ namespace Content.Server.GameTicking
|
||||
return;
|
||||
}
|
||||
|
||||
// Figure out job restrictions
|
||||
var restrictedRoles = new HashSet<string>();
|
||||
|
||||
var getDisallowed = _playTimeTrackings.GetDisallowedJobs(player);
|
||||
restrictedRoles.UnionWith(getDisallowed);
|
||||
|
||||
var jobBans = _roleBanManager.GetJobBans(player.UserId);
|
||||
if(jobBans != null) restrictedRoles.UnionWith(jobBans);
|
||||
|
||||
// Pick best job best on prefs.
|
||||
jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station, character.JobPriorities, true,
|
||||
_roleBanManager.GetJobBans(player.UserId));
|
||||
restrictedRoles);
|
||||
// If no job available, stay in lobby, or if no lobby spawn as observer
|
||||
if (jobId is null)
|
||||
{
|
||||
@@ -161,6 +173,8 @@ namespace Content.Server.GameTicking
|
||||
var job = new Job(newMind, jobPrototype);
|
||||
newMind.AddRole(job);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
if (lateJoin)
|
||||
{
|
||||
_chatSystem.DispatchStationAnnouncement(station,
|
||||
@@ -217,12 +231,11 @@ namespace Content.Server.GameTicking
|
||||
|
||||
public void MakeJoinGame(IPlayerSession player, EntityUid station, string? jobId = null)
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player)) return;
|
||||
|
||||
if (!_prefsManager.HavePreferencesLoaded(player))
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player))
|
||||
return;
|
||||
|
||||
if (!_userDb.IsLoadComplete(player))
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnPlayer(player, station, jobId);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Server.Chat.Systems;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerUpdates;
|
||||
using Content.Server.Station.Systems;
|
||||
@@ -118,5 +119,7 @@ namespace Content.Server.GameTicking
|
||||
[Dependency] private readonly RoleBanManager _roleBanManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly ServerUpdateManager _serverUpdates = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingSystem _playTimeTrackings = default!;
|
||||
[Dependency] private readonly UserDbDataManager _userDb = default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using Content.Server.MoMMI;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Server.Objectives.Interfaces;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.ServerUpdates;
|
||||
using Content.Server.Voting.Managers;
|
||||
@@ -59,6 +60,8 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<GhostKickManager>();
|
||||
IoCManager.Register<ISharedAdminLogManager, AdminLogManager>();
|
||||
IoCManager.Register<IAdminLogManager, AdminLogManager>();
|
||||
IoCManager.Register<PlayTimeTrackingManager>();
|
||||
IoCManager.Register<UserDbDataManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ namespace Content.Server.Mind
|
||||
_roles.Add(role);
|
||||
role.Greet();
|
||||
|
||||
var message = new RoleAddedEvent(role);
|
||||
var message = new RoleAddedEvent(this, role);
|
||||
if (OwnedEntity != null)
|
||||
{
|
||||
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(OwnedEntity.Value, message, true);
|
||||
@@ -203,7 +203,7 @@ namespace Content.Server.Mind
|
||||
|
||||
_roles.Remove(role);
|
||||
|
||||
var message = new RoleRemovedEvent(role);
|
||||
var message = new RoleRemovedEvent(this, role);
|
||||
|
||||
if (OwnedEntity != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Players.PlayTimeTracking;
|
||||
|
||||
public delegate void CalcPlayTimeTrackersCallback(IPlayerSession player, HashSet<string> trackers);
|
||||
|
||||
/// <summary>
|
||||
/// Tracks play time for players, across all roles.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Play time is tracked in distinct "trackers" (defined in <see cref="PlayTimeTrackerPrototype"/>).
|
||||
/// Most jobs correspond to one such tracker, but there are also more trackers like <c>"Overall"</c> which tracks cumulative playtime across all roles.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To actually figure out what trackers are active, <see cref="CalcTrackers"/> is invoked in a "refresh".
|
||||
/// The next time the trackers are refreshed, these trackers all get the time since the last refresh added.
|
||||
/// Refreshes are triggered by <see cref="QueueRefreshTrackers"/>, and should be raised through events such as players' roles changing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because the calculation system does not persistently keep ticking timers,
|
||||
/// APIs like <see cref="GetPlayTimeForTracker"/> will not see live-updating information.
|
||||
/// A light-weight form of refresh is a "flush" through <see cref="FlushTracker"/>.
|
||||
/// This will not cause active trackers to be re-calculated like a refresh,
|
||||
/// but it will ensure stored play time info is up to date.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Trackers are auto-saved to DB on a cvar-configured interval. This interval is independent of refreshes,
|
||||
/// but does do a flush to get the latest info.
|
||||
/// Some things like round restarts and player disconnects cause immediate saving of one or all sessions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Tracker data is loaded from the database when the client connects as part of <see cref="UserDbDataManager"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Timing logic in this manager is ran **out** of simulation.
|
||||
/// This means that we use real time, not simulation time, for timing everything here.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Operations like refreshing and sending play time info to clients are deferred until the next frame (note: not tick).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class PlayTimeTrackingManager
|
||||
{
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly IServerNetManager _net = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ITaskManager _task = default!;
|
||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
// List of players that need some kind of update (refresh timers or resend).
|
||||
private ValueList<IPlayerSession> _playersDirty;
|
||||
|
||||
// DB auto-saving logic.
|
||||
private TimeSpan _saveInterval;
|
||||
private TimeSpan _lastSave;
|
||||
|
||||
// List of pending DB save operations.
|
||||
// We must block server shutdown on these to avoid losing data.
|
||||
private readonly List<Task> _pendingSaveTasks = new();
|
||||
|
||||
private readonly Dictionary<IPlayerSession, PlayTimeData> _playTimeData = new();
|
||||
|
||||
public event CalcPlayTimeTrackersCallback? CalcTrackers;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("play_time");
|
||||
|
||||
_net.RegisterNetMessage<MsgPlayTime>();
|
||||
|
||||
_cfg.OnValueChanged(CCVars.PlayTimeSaveInterval, f => _saveInterval = TimeSpan.FromSeconds(f), true);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
Save();
|
||||
|
||||
_task.BlockWaitOnTask(Task.WhenAll(_pendingSaveTasks));
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
// NOTE: This is run **out** of simulation. This is intentional.
|
||||
|
||||
UpdateDirtyPlayers();
|
||||
|
||||
if (_timing.RealTime < _lastSave + _saveInterval)
|
||||
return;
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private void UpdateDirtyPlayers()
|
||||
{
|
||||
if (_playersDirty.Count == 0)
|
||||
return;
|
||||
|
||||
var time = _timing.RealTime;
|
||||
|
||||
foreach (var player in _playersDirty)
|
||||
{
|
||||
if (!_playTimeData.TryGetValue(player, out var data))
|
||||
continue;
|
||||
|
||||
DebugTools.Assert(data.IsDirty);
|
||||
|
||||
if (data.NeedRefreshTackers)
|
||||
{
|
||||
RefreshSingleTracker(player, data, time);
|
||||
data.NeedRefreshTackers = true;
|
||||
}
|
||||
|
||||
if (data.NeedSendTimers)
|
||||
{
|
||||
SendPlayTimes(player);
|
||||
data.NeedSendTimers = false;
|
||||
}
|
||||
|
||||
data.IsDirty = false;
|
||||
}
|
||||
|
||||
_playersDirty.Clear();
|
||||
}
|
||||
|
||||
private void RefreshSingleTracker(IPlayerSession dirty, PlayTimeData data, TimeSpan time)
|
||||
{
|
||||
DebugTools.Assert(data.Initialized);
|
||||
|
||||
FlushSingleTracker(data, time);
|
||||
|
||||
data.NeedRefreshTackers = false;
|
||||
|
||||
data.ActiveTrackers.Clear();
|
||||
|
||||
// Fetch new trackers.
|
||||
// Inside try catch to avoid state corruption from bad callback code.
|
||||
try
|
||||
{
|
||||
CalcTrackers?.Invoke(dirty, data.ActiveTrackers);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_runtimeLog.LogException(e, "PlayTime CalcTrackers");
|
||||
data.ActiveTrackers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush all trackers for all players.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlushTracker"/>
|
||||
public void FlushAllTrackers()
|
||||
{
|
||||
var time = _timing.RealTime;
|
||||
|
||||
foreach (var data in _playTimeData.Values)
|
||||
{
|
||||
FlushSingleTracker(data, time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush time tracker information for a player,
|
||||
/// so APIs like <see cref="GetPlayTimeForTracker"/> return up-to-date info.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlushAllTrackers"/>
|
||||
public void FlushTracker(IPlayerSession player)
|
||||
{
|
||||
var time = _timing.RealTime;
|
||||
var data = _playTimeData[player];
|
||||
|
||||
FlushSingleTracker(data, time);
|
||||
}
|
||||
|
||||
private static void FlushSingleTracker(PlayTimeData data, TimeSpan time)
|
||||
{
|
||||
var delta = time - data.LastUpdate;
|
||||
data.LastUpdate = time;
|
||||
|
||||
// Flush active trackers into semi-permanent storage.
|
||||
foreach (var active in data.ActiveTrackers)
|
||||
{
|
||||
AddTimeToTracker(data, active, delta);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendPlayTimes(IPlayerSession pSession)
|
||||
{
|
||||
var roles = GetTrackerTimes(pSession);
|
||||
|
||||
var msg = new MsgPlayTime
|
||||
{
|
||||
Trackers = roles
|
||||
};
|
||||
|
||||
_net.ServerSendMessage(msg, pSession.ConnectedClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save all modified time trackers for all players to the database.
|
||||
/// </summary>
|
||||
public async void Save()
|
||||
{
|
||||
FlushAllTrackers();
|
||||
|
||||
_lastSave = _timing.RealTime;
|
||||
|
||||
TrackPending(DoSaveAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save all modified time trackers for a player to the database.
|
||||
/// </summary>
|
||||
public async void SaveSession(IPlayerSession session)
|
||||
{
|
||||
// This causes all trackers to refresh, ah well.
|
||||
FlushAllTrackers();
|
||||
|
||||
TrackPending(DoSaveSessionAsync(session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Track a database save task to make sure we block server shutdown on it.
|
||||
/// </summary>
|
||||
private async void TrackPending(Task task)
|
||||
{
|
||||
_pendingSaveTasks.Add(task);
|
||||
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingSaveTasks.Remove(task);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoSaveAsync()
|
||||
{
|
||||
var log = new List<PlayTimeUpdate>();
|
||||
|
||||
foreach (var (player, data) in _playTimeData)
|
||||
{
|
||||
foreach (var tracker in data.DbTrackersDirty)
|
||||
{
|
||||
log.Add(new PlayTimeUpdate(player.UserId, tracker, data.TrackerTimes[tracker]));
|
||||
}
|
||||
|
||||
data.DbTrackersDirty.Clear();
|
||||
}
|
||||
|
||||
if (log.Count == 0)
|
||||
return;
|
||||
|
||||
// NOTE: we do replace updates here, not incremental additions.
|
||||
// This means that if you're playing on two servers at the same time, they'll step on each other's feet.
|
||||
// This is considered fine.
|
||||
await _db.UpdatePlayTimes(log);
|
||||
|
||||
_sawmill.Debug($"Saved {log.Count} trackers");
|
||||
}
|
||||
|
||||
private async Task DoSaveSessionAsync(IPlayerSession session)
|
||||
{
|
||||
var log = new List<PlayTimeUpdate>();
|
||||
|
||||
var data = _playTimeData[session];
|
||||
|
||||
foreach (var tracker in data.DbTrackersDirty)
|
||||
{
|
||||
log.Add(new PlayTimeUpdate(session.UserId, tracker, data.TrackerTimes[tracker]));
|
||||
}
|
||||
|
||||
data.DbTrackersDirty.Clear();
|
||||
|
||||
// NOTE: we do replace updates here, not incremental additions.
|
||||
// This means that if you're playing on two servers at the same time, they'll step on each other's feet.
|
||||
// This is considered fine.
|
||||
await _db.UpdatePlayTimes(log);
|
||||
|
||||
_sawmill.Debug($"Saved {log.Count} trackers for {session.Name}");
|
||||
}
|
||||
|
||||
public async Task LoadData(IPlayerSession session, CancellationToken cancel)
|
||||
{
|
||||
var data = new PlayTimeData();
|
||||
_playTimeData.Add(session, data);
|
||||
|
||||
var playTimes = await _db.GetPlayTimes(session.UserId);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var timer in playTimes)
|
||||
{
|
||||
data.TrackerTimes.Add(timer.Tracker, timer.TimeSpent);
|
||||
}
|
||||
|
||||
data.Initialized = true;
|
||||
|
||||
QueueRefreshTrackers(session);
|
||||
QueueSendTimers(session);
|
||||
}
|
||||
|
||||
public void ClientDisconnected(IPlayerSession session)
|
||||
{
|
||||
SaveSession(session);
|
||||
|
||||
_playTimeData.Remove(session);
|
||||
}
|
||||
|
||||
public void AddTimeToTracker(IPlayerSession id, string tracker, TimeSpan time)
|
||||
{
|
||||
if (!_playTimeData.TryGetValue(id, out var data) || !data.Initialized)
|
||||
throw new InvalidOperationException("Play time info is not yet loaded for this player!");
|
||||
|
||||
AddTimeToTracker(data, tracker, time);
|
||||
}
|
||||
|
||||
private static void AddTimeToTracker(PlayTimeData data, string tracker, TimeSpan time)
|
||||
{
|
||||
ref var timer = ref CollectionsMarshal.GetValueRefOrAddDefault(data.TrackerTimes, tracker, out _);
|
||||
timer += time;
|
||||
|
||||
data.DbTrackersDirty.Add(tracker);
|
||||
}
|
||||
|
||||
public void AddTimeToOverallPlaytime(IPlayerSession id, TimeSpan time)
|
||||
{
|
||||
AddTimeToTracker(id, PlayTimeTrackingShared.TrackerOverall, time);
|
||||
}
|
||||
|
||||
public TimeSpan GetOverallPlaytime(IPlayerSession id)
|
||||
{
|
||||
return GetPlayTimeForTracker(id, PlayTimeTrackingShared.TrackerOverall);
|
||||
}
|
||||
|
||||
public Dictionary<string, TimeSpan> GetTrackerTimes(IPlayerSession id)
|
||||
{
|
||||
if (!_playTimeData.TryGetValue(id, out var data) || !data.Initialized)
|
||||
throw new InvalidOperationException("Play time info is not yet loaded for this player!");
|
||||
|
||||
return data.TrackerTimes;
|
||||
}
|
||||
|
||||
public TimeSpan GetPlayTimeForTracker(IPlayerSession id, string tracker)
|
||||
{
|
||||
if (!_playTimeData.TryGetValue(id, out var data) || !data.Initialized)
|
||||
throw new InvalidOperationException("Play time info is not yet loaded for this player!");
|
||||
|
||||
return data.TrackerTimes.GetValueOrDefault(tracker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue for play time trackers to be refreshed on a player, in case the set of active trackers may have changed.
|
||||
/// </summary>
|
||||
public void QueueRefreshTrackers(IPlayerSession player)
|
||||
{
|
||||
if (DirtyPlayer(player) is { } data)
|
||||
data.NeedRefreshTackers = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue for play time information to be sent to a client, for showing in UIs etc.
|
||||
/// </summary>
|
||||
public void QueueSendTimers(IPlayerSession player)
|
||||
{
|
||||
if (DirtyPlayer(player) is { } data)
|
||||
data.NeedSendTimers = true;
|
||||
}
|
||||
|
||||
private PlayTimeData? DirtyPlayer(IPlayerSession player)
|
||||
{
|
||||
if (!_playTimeData.TryGetValue(player, out var data) || !data.Initialized)
|
||||
return null;
|
||||
|
||||
if (!data.IsDirty)
|
||||
{
|
||||
data.IsDirty = true;
|
||||
_playersDirty.Add(player);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play time info for a particular player.
|
||||
/// </summary>
|
||||
private sealed class PlayTimeData
|
||||
{
|
||||
// Queued update flags
|
||||
public bool IsDirty;
|
||||
public bool NeedRefreshTackers;
|
||||
public bool NeedSendTimers;
|
||||
|
||||
// Active tracking info
|
||||
public readonly HashSet<string> ActiveTrackers = new();
|
||||
public TimeSpan LastUpdate;
|
||||
|
||||
// Stored tracked time info.
|
||||
|
||||
/// <summary>
|
||||
/// Have we finished retrieving our data from the DB?
|
||||
/// </summary>
|
||||
public bool Initialized;
|
||||
|
||||
public readonly Dictionary<string, TimeSpan> TrackerTimes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of trackers which are different from their DB values and need to be saved to DB.
|
||||
/// </summary>
|
||||
public readonly HashSet<string> DbTrackersDirty = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Afk;
|
||||
using Content.Server.Afk.Events;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.MobState;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Players.PlayTimeTracking;
|
||||
|
||||
/// <summary>
|
||||
/// Connects <see cref="PlayTimeTrackingManager"/> to the simulation state. Reports trackers and such.
|
||||
/// </summary>
|
||||
public sealed class PlayTimeTrackingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAfkManager _afk = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _tracking = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_tracking.CalcTrackers += CalcTrackers;
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundEnd);
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<RoleAddedEvent>(OnRoleAdd);
|
||||
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleRemove);
|
||||
SubscribeLocalEvent<AFKEvent>(OnAFK);
|
||||
SubscribeLocalEvent<UnAFKEvent>(OnUnAFK);
|
||||
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_tracking.CalcTrackers -= CalcTrackers;
|
||||
}
|
||||
|
||||
private void CalcTrackers(IPlayerSession player, HashSet<string> trackers)
|
||||
{
|
||||
if (_afk.IsAfk(player))
|
||||
return;
|
||||
|
||||
if (!IsPlayerAlive(player))
|
||||
return;
|
||||
|
||||
trackers.Add(PlayTimeTrackingShared.TrackerOverall);
|
||||
trackers.UnionWith(GetTimedRoles(player));
|
||||
}
|
||||
|
||||
private bool IsPlayerAlive(IPlayerSession session)
|
||||
{
|
||||
var attached = session.AttachedEntity;
|
||||
if (attached == null)
|
||||
return false;
|
||||
|
||||
if (!TryComp<MobStateComponent>(attached, out var state))
|
||||
return false;
|
||||
|
||||
return state.CurrentState is DamageState.Alive or DamageState.Critical;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetTimedRoles(Mind.Mind mind)
|
||||
{
|
||||
foreach (var role in mind.AllRoles)
|
||||
{
|
||||
if (role is not IRoleTimer timer)
|
||||
continue;
|
||||
|
||||
yield return _prototypes.Index<PlayTimeTrackerPrototype>(timer.Timer).ID;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetTimedRoles(IPlayerSession session)
|
||||
{
|
||||
var contentData = _playerManager.GetPlayerData(session.UserId).ContentData();
|
||||
|
||||
if (contentData?.Mind == null)
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
return GetTimedRoles(contentData.Mind);
|
||||
}
|
||||
|
||||
private void OnRoleRemove(RoleRemovedEvent ev)
|
||||
{
|
||||
if (ev.Mind.Session == null)
|
||||
return;
|
||||
|
||||
_tracking.QueueRefreshTrackers(ev.Mind.Session);
|
||||
}
|
||||
|
||||
private void OnRoleAdd(RoleAddedEvent ev)
|
||||
{
|
||||
if (ev.Mind.Session == null)
|
||||
return;
|
||||
|
||||
_tracking.QueueRefreshTrackers(ev.Mind.Session);
|
||||
}
|
||||
|
||||
private void OnRoundEnd(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_tracking.Save();
|
||||
}
|
||||
|
||||
private void OnUnAFK(ref UnAFKEvent ev)
|
||||
{
|
||||
_tracking.QueueRefreshTrackers(ev.Session);
|
||||
}
|
||||
|
||||
private void OnAFK(ref AFKEvent ev)
|
||||
{
|
||||
_tracking.QueueRefreshTrackers(ev.Session);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(PlayerAttachedEvent ev)
|
||||
{
|
||||
_tracking.QueueRefreshTrackers(ev.Player);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(PlayerDetachedEvent ev)
|
||||
{
|
||||
// This doesn't fire if the player doesn't leave their body. I guess it's fine?
|
||||
_tracking.QueueRefreshTrackers(ev.Player);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(MobStateChangedEvent ev)
|
||||
{
|
||||
if (!TryComp(ev.Entity, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
_tracking.QueueRefreshTrackers(actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void OnPlayerJoinedLobby(PlayerJoinedLobbyEvent ev)
|
||||
{
|
||||
_tracking.QueueRefreshTrackers(ev.PlayerSession);
|
||||
// Send timers to client when they join lobby, so the UIs are up-to-date.
|
||||
_tracking.QueueSendTimers(ev.PlayerSession);
|
||||
}
|
||||
|
||||
public bool IsAllowed(IPlayerSession player, string role)
|
||||
{
|
||||
if (!_prototypes.TryIndex<JobPrototype>(role, out var job) ||
|
||||
job.Requirements == null ||
|
||||
!_cfg.GetCVar(CCVars.GameRoleTimers))
|
||||
return true;
|
||||
|
||||
var playTimes = _tracking.GetTrackerTimes(player);
|
||||
|
||||
return JobRequirements.TryRequirementsMet(job, playTimes, out _, _prototypes);
|
||||
}
|
||||
|
||||
public HashSet<string> GetDisallowedJobs(IPlayerSession player)
|
||||
{
|
||||
var roles = new HashSet<string>();
|
||||
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
|
||||
return roles;
|
||||
|
||||
var playTimes = _tracking.GetTrackerTimes(player);
|
||||
|
||||
foreach (var job in _prototypes.EnumeratePrototypes<JobPrototype>())
|
||||
{
|
||||
if (job.Requirements != null)
|
||||
{
|
||||
foreach (var requirement in job.Requirements)
|
||||
{
|
||||
if (JobRequirements.TryRequirementMet(requirement, playTimes, out _, _prototypes))
|
||||
continue;
|
||||
|
||||
goto NoRole;
|
||||
}
|
||||
}
|
||||
|
||||
roles.Add(job.ID);
|
||||
NoRole:;
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void RemoveDisallowedJobs(NetUserId userId, ref List<string> jobs)
|
||||
{
|
||||
if (!_cfg.GetCVar(CCVars.GameRoleTimers))
|
||||
return;
|
||||
|
||||
var player = _playerManager.GetSessionByUserId(userId);
|
||||
var playTimes = _tracking.GetTrackerTimes(player);
|
||||
|
||||
for (var i = 0; i < jobs.Count; i++)
|
||||
{
|
||||
var job = jobs[i];
|
||||
|
||||
if (!_prototypes.TryIndex<JobPrototype>(job, out var jobber) ||
|
||||
jobber.Requirements == null ||
|
||||
jobber.Requirements.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var requirement in jobber.Requirements)
|
||||
{
|
||||
if (JobRequirements.TryRequirementMet(requirement, playTimes, out _, _prototypes))
|
||||
continue;
|
||||
|
||||
jobs.RemoveSwap(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayerRolesChanged(IPlayerSession player)
|
||||
{
|
||||
_tracking.QueueRefreshTrackers(player);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Server.Player;
|
||||
@@ -9,12 +10,9 @@ namespace Content.Server.Preferences.Managers
|
||||
{
|
||||
void Init();
|
||||
|
||||
void OnClientConnected(IPlayerSession session);
|
||||
Task LoadData(IPlayerSession session, CancellationToken cancel);
|
||||
void OnClientDisconnected(IPlayerSession session);
|
||||
|
||||
bool HavePreferencesLoaded(IPlayerSession session);
|
||||
Task WaitPreferencesLoaded(IPlayerSession session);
|
||||
|
||||
PlayerPreferences GetPreferences(NetUserId userId);
|
||||
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -43,7 +44,7 @@ namespace Content.Server.Preferences.Managers
|
||||
var index = message.SelectedCharacterIndex;
|
||||
var userId = message.MsgChannel.UserId;
|
||||
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded.IsCompleted)
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
|
||||
{
|
||||
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
|
||||
return;
|
||||
@@ -83,7 +84,7 @@ namespace Content.Server.Preferences.Managers
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded.IsCompleted)
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
|
||||
{
|
||||
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
|
||||
return;
|
||||
@@ -116,7 +117,7 @@ namespace Content.Server.Preferences.Managers
|
||||
var slot = message.Slot;
|
||||
var userId = message.MsgChannel.UserId;
|
||||
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded.IsCompleted)
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
|
||||
{
|
||||
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
|
||||
return;
|
||||
@@ -163,14 +164,15 @@ namespace Content.Server.Preferences.Managers
|
||||
}
|
||||
}
|
||||
|
||||
public async void OnClientConnected(IPlayerSession session)
|
||||
// Should only be called via UserDbDataManager.
|
||||
public async Task LoadData(IPlayerSession session, CancellationToken cancel)
|
||||
{
|
||||
if (!ShouldStorePrefs(session.ConnectedClient.AuthType))
|
||||
{
|
||||
// Don't store data for guests.
|
||||
var prefsData = new PlayerPrefData
|
||||
{
|
||||
PrefsLoaded = Task.CompletedTask,
|
||||
PrefsLoaded = true,
|
||||
Prefs = new PlayerPreferences(
|
||||
new[] {new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random())},
|
||||
0, Color.Transparent)
|
||||
@@ -182,7 +184,6 @@ namespace Content.Server.Preferences.Managers
|
||||
{
|
||||
var prefsData = new PlayerPrefData();
|
||||
var loadTask = LoadPrefs();
|
||||
prefsData.PrefsLoaded = loadTask;
|
||||
_cachedPlayerPrefs[session.UserId] = prefsData;
|
||||
|
||||
await loadTask;
|
||||
@@ -191,6 +192,7 @@ namespace Content.Server.Preferences.Managers
|
||||
{
|
||||
var prefs = await GetOrCreatePreferencesAsync(session.UserId);
|
||||
prefsData.Prefs = prefs;
|
||||
prefsData.PrefsLoaded = true;
|
||||
|
||||
var msg = new MsgPreferencesAndSettings();
|
||||
msg.Preferences = prefs;
|
||||
@@ -203,7 +205,6 @@ namespace Content.Server.Preferences.Managers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void OnClientDisconnected(IPlayerSession session)
|
||||
{
|
||||
_cachedPlayerPrefs.Remove(session.UserId);
|
||||
@@ -214,11 +215,6 @@ namespace Content.Server.Preferences.Managers
|
||||
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.
|
||||
@@ -303,7 +299,7 @@ namespace Content.Server.Preferences.Managers
|
||||
|
||||
private sealed class PlayerPrefData
|
||||
{
|
||||
public Task PrefsLoaded = default!;
|
||||
public bool PrefsLoaded;
|
||||
public PlayerPreferences? Prefs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Content.Server.Chat;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Roles
|
||||
{
|
||||
public sealed class Job : Role
|
||||
public sealed class Job : Role, IRoleTimer
|
||||
{
|
||||
[ViewVariables] public string Timer => Prototype.PlayTimeTracker;
|
||||
|
||||
[ViewVariables]
|
||||
public JobPrototype Prototype { get; }
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
{
|
||||
public sealed class RoleAddedEvent : RoleEvent
|
||||
{
|
||||
public RoleAddedEvent(Role role) : base(role) { }
|
||||
public RoleAddedEvent(Mind.Mind mind, Role role) : base(mind, role) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
{
|
||||
public abstract class RoleEvent : EntityEventArgs
|
||||
{
|
||||
public readonly Mind.Mind Mind;
|
||||
public readonly Role Role;
|
||||
|
||||
public RoleEvent(Role role)
|
||||
public RoleEvent(Mind.Mind mind, Role role)
|
||||
{
|
||||
Mind = mind;
|
||||
Role = role;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
{
|
||||
public sealed class RoleRemovedEvent : RoleEvent
|
||||
{
|
||||
public RoleRemovedEvent(Role role) : base(role) { }
|
||||
public RoleRemovedEvent(Mind.Mind mind, Role role) : base(mind, role) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
@@ -15,6 +17,7 @@ public sealed partial class StationJobsSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly RoleBanManager _roleBanManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingSystem _playTime = default!;
|
||||
|
||||
private Dictionary<int, HashSet<string>> _jobsByWeight = default!;
|
||||
private List<int> _orderedWeights = default!;
|
||||
@@ -339,11 +342,15 @@ public sealed partial class StationJobsSystem
|
||||
foreach (var (player, profile) in profiles)
|
||||
{
|
||||
var roleBans = _roleBanManager.GetJobBans(player);
|
||||
var profileJobs = profile.JobPriorities.Keys.ToList();
|
||||
_playTime.RemoveDisallowedJobs(player, ref profileJobs);
|
||||
|
||||
List<string>? availableJobs = null;
|
||||
|
||||
foreach (var (jobId, priority) in profile.JobPriorities)
|
||||
foreach (var jobId in profileJobs)
|
||||
{
|
||||
var priority = profile.JobPriorities[jobId];
|
||||
|
||||
if (!(priority == selectedPriority || selectedPriority is null))
|
||||
continue;
|
||||
|
||||
@@ -357,7 +364,6 @@ public sealed partial class StationJobsSystem
|
||||
continue;
|
||||
|
||||
availableJobs ??= new List<string>(profile.JobPriorities.Count);
|
||||
|
||||
availableJobs.Add(jobId);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.GameTicking;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
Reference in New Issue
Block a user