fix antag selection being evil (#28197)

* fix antag selection being evil

* fix test

* untroll the other tests

* remove role timer troll

* Allow tests to modify antag preferences

* Fix antag selection

* Misc test fixes

* Add AntagPreferenceTest

* Fix lazy mistakes

* Test cleanup

* Try stop players in lobbies from being assigned mid-round antags

* ranting

* I am going insane

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
This commit is contained in:
deltanedas
2024-05-26 05:14:29 +00:00
committed by GitHub
parent af10de61d3
commit 492ccc93d0
13 changed files with 182 additions and 61 deletions

View File

@@ -2,6 +2,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -128,4 +131,29 @@ public sealed partial class TestPair
return list; return list;
} }
/// <summary>
/// Helper method for enabling or disabling a antag role
/// </summary>
public async Task SetAntagPref(ProtoId<AntagPrototype> id, bool value)
{
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(Client.User!.Value);
// what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable?
var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter;
Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value));
var newProfile = profile.WithAntagPreference(id, value);
await Server.WaitPost(() =>
{
prefMan.SetProfile(Client.User.Value, prefs.SelectedCharacterIndex, newProfile).Wait();
});
// And why the fuck does it always create a new preference and profile object instead of just reusing them?
var newPrefs = prefMan.GetPreferences(Client.User.Value);
var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter;
Assert.That(newProf.AntagPreferences.Contains(id), Is.EqualTo(value));
}
} }

View File

@@ -65,11 +65,11 @@ public static partial class PoolManager
options.BeforeStart += () => options.BeforeStart += () =>
{ {
// Server-only systems (i.e., systems that subscribe to events with server-only components)
var entSysMan = IoCManager.Resolve<IEntitySystemManager>(); var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<ResettingEntitySystemTests.TestRoundRestartCleanupEvent>();
entSysMan.LoadExtraSystemType<InteractionSystemTests.TestInteractionSystem>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>(); entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>(); entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error; IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve<IConfigurationManager>() IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true); .OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);

View File

@@ -0,0 +1,76 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Antag;
using Content.Server.Antag.Components;
using Content.Server.GameTicking;
using Content.Shared.GameTicking;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.IntegrationTests.Tests.GameRules;
// Once upon a time, players in the lobby weren't ever considered eligible for antag roles.
// Lets not let that happen again.
[TestFixture]
public sealed class AntagPreferenceTest
{
[Test]
public async Task TestLobbyPlayersValid()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
var server = pair.Server;
var client = pair.Client;
var ticker = server.System<GameTicker>();
var sys = server.System<AntagSelectionSystem>();
// Initially in the lobby
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
EntityUid uid = default;
await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor"));
var rule = new Entity<AntagSelectionComponent>(uid, server.EntMan.GetComponent<AntagSelectionComponent>(uid));
var def = rule.Comp.Definitions.Single();
// IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby.
// Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that.
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
// By default, traitor/antag preferences are disabled, so the pool should be empty.
var sessions = new List<ICommonSession>{pair.Player!};
var pool = sys.GetPlayerPool(rule, sessions, def);
Assert.That(pool.Count, Is.EqualTo(0));
// Opt into the traitor role.
await pair.SetAntagPref("Traitor", true);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
pool = sys.GetPlayerPool(rule, sessions, def);
Assert.That(pool.Count, Is.EqualTo(1));
pool.TryPickAndTake(pair.Server.ResolveDependency<IRobustRandom>(), out var picked);
Assert.That(picked, Is.EqualTo(pair.Player));
Assert.That(sessions.Count, Is.EqualTo(1));
// opt back out
await pair.SetAntagPref("Traitor", false);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
pool = sys.GetPlayerPool(rule, sessions, def);
Assert.That(pool.Count, Is.EqualTo(0));
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.CleanReturnAsync();
}
}

View File

@@ -58,6 +58,9 @@ public sealed class NukeOpsTest
Assert.That(client.AttachedEntity, Is.Null); Assert.That(client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay)); Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
// Opt into the nukies role.
await pair.SetAntagPref("NukeopsCommander", true);
// There are no grids or maps // There are no grids or maps
Assert.That(entMan.Count<MapComponent>(), Is.Zero); Assert.That(entMan.Count<MapComponent>(), Is.Zero);
Assert.That(entMan.Count<MapGridComponent>(), Is.Zero); Assert.That(entMan.Count<MapGridComponent>(), Is.Zero);
@@ -198,6 +201,7 @@ public sealed class NukeOpsTest
ticker.SetGamePreset((GamePresetPrototype?)null); ticker.SetGamePreset((GamePresetPrototype?)null);
server.CfgMan.SetCVar(CCVars.GridFill, false); server.CfgMan.SetCVar(CCVars.GridFill, false);
await pair.SetAntagPref("NukeopsCommander", false);
await pair.CleanReturnAsync(); await pair.CleanReturnAsync();
} }
} }

View File

@@ -407,7 +407,6 @@ namespace Content.IntegrationTests.Tests.Interaction.Click
await pair.CleanReturnAsync(); await pair.CleanReturnAsync();
} }
[Reflect(false)]
public sealed class TestInteractionSystem : EntitySystem public sealed class TestInteractionSystem : EntitySystem
{ {
public EntityEventHandler<InteractUsingEvent>? InteractUsingEvent; public EntityEventHandler<InteractUsingEvent>? InteractUsingEvent;

View File

@@ -9,7 +9,6 @@ namespace Content.IntegrationTests.Tests
[TestOf(typeof(RoundRestartCleanupEvent))] [TestOf(typeof(RoundRestartCleanupEvent))]
public sealed class ResettingEntitySystemTests public sealed class ResettingEntitySystemTests
{ {
[Reflect(false)]
public sealed class TestRoundRestartCleanupEvent : EntitySystem public sealed class TestRoundRestartCleanupEvent : EntitySystem
{ {
public bool HasBeenReset { get; set; } public bool HasBeenReset { get; set; }
@@ -49,8 +48,6 @@ namespace Content.IntegrationTests.Tests
system.HasBeenReset = false; system.HasBeenReset = false;
Assert.That(system.HasBeenReset, Is.False);
gameTicker.RestartRound(); gameTicker.RestartRound();
Assert.That(system.HasBeenReset); Assert.That(system.HasBeenReset);

View File

@@ -27,6 +27,11 @@ public sealed partial class AntagSelectionSystem
if (mindCount >= totalTargetCount) if (mindCount >= totalTargetCount)
return false; return false;
// TODO ANTAG fix this
// If here are two definitions with 1/10 and 10/10 slots filled, this will always return the second definition
// even though it has already met its target
// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA I fucking hate game ticker code.
// It needs to track selected minds for each definition independently.
foreach (var def in ent.Comp.Definitions) foreach (var def in ent.Comp.Definitions)
{ {
var target = GetTargetAntagCount(ent, null, def); var target = GetTargetAntagCount(ent, null, def);
@@ -64,6 +69,10 @@ public sealed partial class AntagSelectionSystem
/// </summary> /// </summary>
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def) public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
{ {
// TODO ANTAG
// make pool non-nullable
// Review uses and ensure that people are INTENTIONALLY including players in the lobby if this is a mid-round
// antag selection.
var poolSize = pool?.Count ?? _playerManager.Sessions var poolSize = pool?.Count ?? _playerManager.Sessions
.Count(s => s.State.Status is not SessionStatus.Disconnected and not SessionStatus.Zombie); .Count(s => s.State.Status is not SessionStatus.Disconnected and not SessionStatus.Zombie);

View File

@@ -14,6 +14,7 @@ using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Shared.Antag; using Content.Shared.Antag;
using Content.Shared.GameTicking;
using Content.Shared.Ghost; using Content.Shared.Ghost;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Players; using Content.Shared.Players;
@@ -25,6 +26,7 @@ using Robust.Shared.Enums;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Antag; namespace Content.Server.Antag;
@@ -85,10 +87,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
continue; continue;
if (comp.SelectionsComplete) if (comp.SelectionsComplete)
return; continue;
ChooseAntags((uid, comp), pool); ChooseAntags((uid, comp), pool);
comp.SelectionsComplete = true;
foreach (var session in comp.SelectedSessions) foreach (var session in comp.SelectedSessions)
{ {
@@ -106,11 +107,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn) if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
continue; continue;
if (comp.SelectionsComplete) ChooseAntags((uid, comp), args.Players);
continue;
ChooseAntags((uid, comp));
comp.SelectionsComplete = true;
} }
} }
@@ -126,12 +123,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules(); var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var antag, out _)) while (query.MoveNext(out var uid, out _, out var antag, out _))
{ {
// TODO ANTAG
// what why aasdiuhasdopiuasdfhksad
// stop this insanity please
// probability of antag assignment shouldn't depend on the order in which rules are returned by the query.
if (!RobustRandom.Prob(LateJoinRandomChance)) if (!RobustRandom.Prob(LateJoinRandomChance))
continue; continue;
if (!antag.Definitions.Any(p => p.LateJoinAdditional)) if (!antag.Definitions.Any(p => p.LateJoinAdditional))
continue; continue;
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
if (!TryGetNextAvailableDefinition((uid, antag), out var def)) if (!TryGetNextAvailableDefinition((uid, antag), out var def))
continue; continue;
@@ -164,43 +167,40 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{ {
base.Started(uid, component, gameRule, args); base.Started(uid, component, gameRule, args);
if (component.SelectionsComplete) // If the round has not yet started, we defer antag selection until roundstart
return;
if (GameTicker.RunLevel != GameRunLevel.InRound) if (GameTicker.RunLevel != GameRunLevel.InRound)
return; return;
if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn) if (component.SelectionsComplete)
return; return;
ChooseAntags((uid, component)); var players = _playerManager.Sessions
component.SelectionsComplete = true; .Where(x => GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame)
} .ToList();
/// <summary> ChooseAntags((uid, component), players);
/// Chooses antagonists from the current selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent)
{
var sessions = _playerManager.Sessions.ToList();
ChooseAntags(ent, sessions);
} }
/// <summary> /// <summary>
/// Chooses antagonists from the given selection of players /// Chooses antagonists from the given selection of players
/// </summary> /// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool) public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool)
{ {
if (ent.Comp.SelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions) foreach (var def in ent.Comp.Definitions)
{ {
ChooseAntags(ent, pool, def); ChooseAntags(ent, pool, def);
} }
ent.Comp.SelectionsComplete = true;
} }
/// <summary> /// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition. /// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary> /// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool, AntagSelectionDefinition def) public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def)
{ {
var playerPool = GetPlayerPool(ent, pool, def); var playerPool = GetPlayerPool(ent, pool, def);
var count = GetTargetAntagCount(ent, playerPool, def); var count = GetTargetAntagCount(ent, playerPool, def);
@@ -324,20 +324,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary> /// <summary>
/// Gets an ordered player pool based on player preferences and the antagonist definition. /// Gets an ordered player pool based on player preferences and the antagonist definition.
/// </summary> /// </summary>
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def) public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, IList<ICommonSession> sessions, AntagSelectionDefinition def)
{ {
var preferredList = new List<ICommonSession>(); var preferredList = new List<ICommonSession>();
var fallbackList = new List<ICommonSession>(); var fallbackList = new List<ICommonSession>();
var unwantedList = new List<ICommonSession>();
var invalidList = new List<ICommonSession>();
foreach (var session in sessions) foreach (var session in sessions)
{ {
if (!IsSessionValid(ent, session, def) || if (!IsSessionValid(ent, session, def) ||
!IsEntityValid(session.AttachedEntity, def)) !IsEntityValid(session.AttachedEntity, def))
{
invalidList.Add(session);
continue; continue;
}
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter; var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
if (def.PrefRoles.Count != 0 && pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p))) if (def.PrefRoles.Count != 0 && pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p)))
@@ -348,13 +343,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{ {
fallbackList.Add(session); fallbackList.Add(session);
} }
else
{
unwantedList.Add(session);
}
} }
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList, unwantedList, invalidList }); return new AntagSelectionPlayerPool(new() { preferredList, fallbackList });
} }
/// <summary> /// <summary>
@@ -365,14 +356,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session == null) if (session == null)
return true; return true;
mind ??= session.GetMind();
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie) if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
return false; return false;
if (ent.Comp.SelectedSessions.Contains(session)) if (ent.Comp.SelectedSessions.Contains(session))
return false; return false;
mind ??= session.GetMind();
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
if (mind == null)
return true;
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds) //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
switch (def.MultiAntagSetting) switch (def.MultiAntagSetting)
@@ -401,10 +396,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary> /// <summary>
/// Checks if a given entity (mind/session not included) is valid for a given antagonist. /// Checks if a given entity (mind/session not included) is valid for a given antagonist.
/// </summary> /// </summary>
private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) public bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
{ {
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
if (entity == null) if (entity == null)
return false; return true;
if (HasComp<PendingClockInComponent>(entity)) if (HasComp<PendingClockInComponent>(entity))
return false; return false;

View File

@@ -795,7 +795,7 @@ namespace Content.Server.GameTicking
} }
/// <summary> /// <summary>
/// Event raised after players were assigned jobs by the GameTicker. /// Event raised after players were assigned jobs by the GameTicker and have been spawned in.
/// You can give on-station people special roles by listening to this event. /// You can give on-station people special roles by listening to this event.
/// </summary> /// </summary>
public sealed class RulePlayerJobsAssignedEvent public sealed class RulePlayerJobsAssignedEvent

View File

@@ -20,5 +20,7 @@ namespace Content.Server.Preferences.Managers
PlayerPreferences? GetPreferencesOrNull(NetUserId? userId); PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds); IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
bool HavePreferencesLoaded(ICommonSession session); bool HavePreferencesLoaded(ICommonSession session);
Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile);
} }
} }

View File

@@ -29,11 +29,14 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IDependencyCollection _dependencies = default!; [Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly ILogManager _log = default!;
// Cache player prefs on the server so we don't need as much async hell related to them. // Cache player prefs on the server so we don't need as much async hell related to them.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs = private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
new(); new();
private ISawmill _sawmill = default!;
private int MaxCharacterSlots => _cfg.GetCVar(CCVars.GameMaxCharacterSlots); private int MaxCharacterSlots => _cfg.GetCVar(CCVars.GameMaxCharacterSlots);
public void Init() public void Init()
@@ -42,6 +45,7 @@ namespace Content.Server.Preferences.Managers
_netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage); _netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage); _netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage);
_netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage); _netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage);
_sawmill = _log.GetSawmill("prefs");
} }
private async void HandleSelectCharacterMessage(MsgSelectCharacter message) private async void HandleSelectCharacterMessage(MsgSelectCharacter message)
@@ -78,27 +82,25 @@ namespace Content.Server.Preferences.Managers
private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message) private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message)
{ {
var slot = message.Slot;
var profile = message.Profile;
var userId = message.MsgChannel.UserId; var userId = message.MsgChannel.UserId;
if (profile == null) // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
{ if (message.Profile == null)
Logger.WarningS("prefs", _sawmill.Error($"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {message.Slot}.");
$"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {slot}."); else
return; await SetProfile(userId, message.Slot, message.Profile);
} }
public async Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile)
{
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded) if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
{ {
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded."); _sawmill.Error($"Tried to modify user {userId} preferences before they loaded.");
return; return;
} }
if (slot < 0 || slot >= MaxCharacterSlots) if (slot < 0 || slot >= MaxCharacterSlots)
{
return; return;
}
var curPrefs = prefsData.Prefs!; var curPrefs = prefsData.Prefs!;
var session = _playerManager.GetSessionById(userId); var session = _playerManager.GetSessionById(userId);
@@ -112,10 +114,8 @@ namespace Content.Server.Preferences.Managers
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor); prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor);
if (ShouldStorePrefs(message.MsgChannel.AuthType)) if (ShouldStorePrefs(session.Channel.AuthType))
{ await _db.SaveCharacterSlotAsync(userId, profile, slot);
await _db.SaveCharacterSlotAsync(message.MsgChannel.UserId, message.Profile, message.Slot);
}
} }
private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message) private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message)

View File

@@ -22,6 +22,14 @@ public enum AntagAcceptability
public enum AntagSelectionTime : byte public enum AntagSelectionTime : byte
{ {
/// <summary>
/// Antag roles are assigned before players are assigned jobs and spawned in.
/// This prevents antag selection from happening if the round is on-going.
/// </summary>
PrePlayerSpawn, PrePlayerSpawn,
/// <summary>
/// Antag roles get assigned after players have been assigned jobs and have spawned in.
/// </summary>
PostPlayerSpawn PostPlayerSpawn
} }

View File

@@ -146,8 +146,10 @@ public abstract class SharedJobSystem : EntitySystem
public bool CanBeAntag(ICommonSession player) public bool CanBeAntag(ICommonSession player)
{ {
// If the player does not have any mind associated with them (e.g., has not spawned in or is in the lobby), then
// they are eligible to be given an antag role/entity.
if (_playerSystem.ContentData(player) is not { Mind: { } mindId }) if (_playerSystem.ContentData(player) is not { Mind: { } mindId })
return false; return true;
if (!MindTryGetJob(mindId, out _, out var prototype)) if (!MindTryGetJob(mindId, out _, out var prototype))
return true; return true;