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

@@ -27,6 +27,11 @@ public sealed partial class AntagSelectionSystem
if (mindCount >= totalTargetCount)
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)
{
var target = GetTargetAntagCount(ent, null, def);
@@ -64,6 +69,10 @@ public sealed partial class AntagSelectionSystem
/// </summary>
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
.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.Station.Systems;
using Content.Shared.Antag;
using Content.Shared.GameTicking;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Players;
@@ -25,6 +26,7 @@ using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Antag;
@@ -85,10 +87,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
continue;
if (comp.SelectionsComplete)
return;
continue;
ChooseAntags((uid, comp), pool);
comp.SelectionsComplete = true;
foreach (var session in comp.SelectedSessions)
{
@@ -106,11 +107,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
continue;
if (comp.SelectionsComplete)
continue;
ChooseAntags((uid, comp));
comp.SelectionsComplete = true;
ChooseAntags((uid, comp), args.Players);
}
}
@@ -126,12 +123,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules();
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))
continue;
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
continue;
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
if (!TryGetNextAvailableDefinition((uid, antag), out var def))
continue;
@@ -164,43 +167,40 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
base.Started(uid, component, gameRule, args);
if (component.SelectionsComplete)
return;
// If the round has not yet started, we defer antag selection until roundstart
if (GameTicker.RunLevel != GameRunLevel.InRound)
return;
if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
if (component.SelectionsComplete)
return;
ChooseAntags((uid, component));
component.SelectionsComplete = true;
}
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame)
.ToList();
/// <summary>
/// Chooses antagonists from the current selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent)
{
var sessions = _playerManager.Sessions.ToList();
ChooseAntags(ent, sessions);
ChooseAntags((uid, component), players);
}
/// <summary>
/// Chooses antagonists from the given selection of players
/// </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)
{
ChooseAntags(ent, pool, def);
}
ent.Comp.SelectionsComplete = true;
}
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </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 count = GetTargetAntagCount(ent, playerPool, def);
@@ -324,20 +324,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Gets an ordered player pool based on player preferences and the antagonist definition.
/// </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 fallbackList = new List<ICommonSession>();
var unwantedList = new List<ICommonSession>();
var invalidList = new List<ICommonSession>();
foreach (var session in sessions)
{
if (!IsSessionValid(ent, session, def) ||
!IsEntityValid(session.AttachedEntity, def))
{
invalidList.Add(session);
continue;
}
var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter;
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);
}
else
{
unwantedList.Add(session);
}
}
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList, unwantedList, invalidList });
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList });
}
/// <summary>
@@ -365,14 +356,18 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session == null)
return true;
mind ??= session.GetMind();
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
return false;
if (ent.Comp.SelectedSessions.Contains(session))
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)
switch (def.MultiAntagSetting)
@@ -401,10 +396,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <summary>
/// Checks if a given entity (mind/session not included) is valid for a given antagonist.
/// </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)
return false;
return true;
if (HasComp<PendingClockInComponent>(entity))
return false;

View File

@@ -795,7 +795,7 @@ namespace Content.Server.GameTicking
}
/// <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.
/// </summary>
public sealed class RulePlayerJobsAssignedEvent

View File

@@ -20,5 +20,7 @@ namespace Content.Server.Preferences.Managers
PlayerPreferences? GetPreferencesOrNull(NetUserId? userId);
IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
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 IPlayerManager _playerManager = 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.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
new();
private ISawmill _sawmill = default!;
private int MaxCharacterSlots => _cfg.GetCVar(CCVars.GameMaxCharacterSlots);
public void Init()
@@ -42,6 +45,7 @@ namespace Content.Server.Preferences.Managers
_netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage);
_netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage);
_sawmill = _log.GetSawmill("prefs");
}
private async void HandleSelectCharacterMessage(MsgSelectCharacter message)
@@ -78,27 +82,25 @@ namespace Content.Server.Preferences.Managers
private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message)
{
var slot = message.Slot;
var profile = message.Profile;
var userId = message.MsgChannel.UserId;
if (profile == null)
{
Logger.WarningS("prefs",
$"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {slot}.");
return;
}
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (message.Profile == null)
_sawmill.Error($"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {message.Slot}.");
else
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)
{
Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded.");
_sawmill.Error($"Tried to modify user {userId} preferences before they loaded.");
return;
}
if (slot < 0 || slot >= MaxCharacterSlots)
{
return;
}
var curPrefs = prefsData.Prefs!;
var session = _playerManager.GetSessionById(userId);
@@ -112,10 +114,8 @@ namespace Content.Server.Preferences.Managers
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor);
if (ShouldStorePrefs(message.MsgChannel.AuthType))
{
await _db.SaveCharacterSlotAsync(message.MsgChannel.UserId, message.Profile, message.Slot);
}
if (ShouldStorePrefs(session.Channel.AuthType))
await _db.SaveCharacterSlotAsync(userId, profile, slot);
}
private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message)