Add Job preference tests (#28625)

* Misc Job related changes

* Add JobTest

* A

* Aa

* Lets not confuse the yaml linter

* fixes

* a
This commit is contained in:
Leon Friedrich
2024-06-06 02:19:24 +12:00
committed by GitHub
parent e1541351a5
commit adeed705e6
44 changed files with 538 additions and 262 deletions

View File

@@ -4,10 +4,12 @@ using Content.Client.Lobby;
using Content.Client.RoundEnd; using Content.Client.RoundEnd;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.GameWindow; using Content.Shared.GameWindow;
using Content.Shared.Roles;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.State; using Robust.Client.State;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.GameTicking.Managers namespace Content.Client.GameTicking.Managers
{ {
@@ -17,10 +19,9 @@ namespace Content.Client.GameTicking.Managers
[Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IClientAdminManager _admin = default!; [Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
private Dictionary<NetEntity, Dictionary<string, uint?>> _jobsAvailable = new(); private Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> _jobsAvailable = new();
private Dictionary<NetEntity, string> _stationNames = new(); private Dictionary<NetEntity, string> _stationNames = new();
[ViewVariables] public bool AreWeReady { get; private set; } [ViewVariables] public bool AreWeReady { get; private set; }
@@ -32,13 +33,13 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public TimeSpan StartTime { get; private set; }
[ViewVariables] public new bool Paused { get; private set; } [ViewVariables] public new bool Paused { get; private set; }
[ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>> JobsAvailable => _jobsAvailable; [ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailable => _jobsAvailable;
[ViewVariables] public IReadOnlyDictionary<NetEntity, string> StationNames => _stationNames; [ViewVariables] public IReadOnlyDictionary<NetEntity, string> StationNames => _stationNames;
public event Action? InfoBlobUpdated; public event Action? InfoBlobUpdated;
public event Action? LobbyStatusUpdated; public event Action? LobbyStatusUpdated;
public event Action? LobbyLateJoinStatusUpdated; public event Action? LobbyLateJoinStatusUpdated;
public event Action<IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>>>? LobbyJobsAvailableUpdated; public event Action<IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>>>? LobbyJobsAvailableUpdated;
public override void Initialize() public override void Initialize()
{ {
@@ -69,7 +70,7 @@ namespace Content.Client.GameTicking.Managers
// reading the console. E.g., logs like this one could leak the nuke station/grid: // reading the console. E.g., logs like this one could leak the nuke station/grid:
// > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470) // > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470)
#if !DEBUG #if !DEBUG
_map.Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning; EntityManager.System<SharedMapSystem>().Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
#endif #endif
} }

View File

@@ -290,7 +290,7 @@ namespace Content.Client.LateJoin
} }
} }
private void JobsAvailableUpdated(IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>> updatedJobs) private void JobsAvailableUpdated(IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> updatedJobs)
{ {
foreach (var stationEntries in updatedJobs) foreach (var stationEntries in updatedJobs)
{ {
@@ -337,10 +337,10 @@ namespace Content.Client.LateJoin
public Label JobLabel { get; } public Label JobLabel { get; }
public string JobId { get; } public string JobId { get; }
public string JobLocalisedName { get; } public string JobLocalisedName { get; }
public uint? Amount { get; private set; } public int? Amount { get; private set; }
private bool _initialised = false; private bool _initialised = false;
public JobButton(Label jobLabel, string jobId, string jobLocalisedName, uint? amount) public JobButton(Label jobLabel, ProtoId<JobPrototype> jobId, string jobLocalisedName, int? amount)
{ {
JobLabel = jobLabel; JobLabel = jobLabel;
JobId = jobId; JobId = jobId;
@@ -350,7 +350,7 @@ namespace Content.Client.LateJoin
_initialised = true; _initialised = true;
} }
public void RefreshLabel(uint? amount) public void RefreshLabel(int? amount)
{ {
if (Amount == amount && _initialised) if (Amount == amount && _initialised)
{ {

View File

@@ -302,7 +302,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
{ {
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key; var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?) // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
return _prototypeManager.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob); return _prototypeManager.Index<JobPrototype>(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob);
} }
public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout) public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)

View File

@@ -53,9 +53,9 @@ public sealed partial class CharacterPickerButton : ContainerButton
.LoadProfileEntity(humanoid, null, true); .LoadProfileEntity(humanoid, null, true);
var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key; var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
if (highPriorityJob != null) if (highPriorityJob != default)
{ {
var jobName = prototypeManager.Index<JobPrototype>(highPriorityJob).LocalizedName; var jobName = prototypeManager.Index(highPriorityJob).LocalizedName;
description = $"{description}\n{jobName}"; description = $"{description}\n{jobName}";
} }
} }

View File

@@ -8,7 +8,6 @@ using Content.Shared.Roles;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.UnitTesting; using Robust.UnitTesting;
@@ -135,32 +134,73 @@ public sealed partial class TestPair
} }
/// <summary> /// <summary>
/// Helper method for enabling or disabling a antag role /// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
/// </summary> /// </summary>
public async Task SetAntagPref(ProtoId<AntagPrototype> id, bool value) public async Task SetAntagPreference(ProtoId<AntagPrototype> id, bool value, NetUserId? user = null)
{ {
await SetAntagPref(Client.User!.Value, id, value); user ??= Client.User!.Value;
if (user is not {} userId)
return;
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(userId);
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
} }
public async Task SetAntagPref(NetUserId user, ProtoId<AntagPrototype> id, bool value) /// <summary>
/// Set a user's job preferences. Modified preferences are automatically reset at the end of the test.
/// </summary>
public async Task SetJobPriority(ProtoId<JobPrototype> id, JobPriority value, NetUserId? user = null)
{ {
user ??= Client.User!.Value;
if (user is { } userId)
await SetJobPriorities(userId, (id, value));
}
/// <inheritdoc cref="SetJobPriority"/>
public async Task SetJobPriorities(params (ProtoId<JobPrototype>, JobPriority)[] priorities)
=> await SetJobPriorities(Client.User!.Value, priorities);
/// <inheritdoc cref="SetJobPriority"/>
public async Task SetJobPriorities(NetUserId user, params (ProtoId<JobPrototype>, JobPriority)[] priorities)
{
var highCount = priorities.Count(x => x.Item2 == JobPriority.High);
Assert.That(highCount, Is.LessThanOrEqualTo(1), "Cannot have more than one high priority job");
var prefMan = Server.ResolveDependency<IServerPreferencesManager>(); var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(user); var prefs = prefMan.GetPreferences(user);
// what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable? var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter; var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value)); // Automatic preference resetting only resets slot 0.
var newProfile = profile.WithAntagPreference(id, value); Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
await Server.WaitPost(() => if (highCount != 0)
{ {
prefMan.SetProfile(user, prefs.SelectedCharacterIndex, newProfile).Wait(); foreach (var (key, priority) in dictionary)
}); {
if (priority == JobPriority.High)
dictionary[key] = JobPriority.Medium;
}
}
// And why the fuck does it always create a new preference and profile object instead of just reusing them? foreach (var (job, priority) in priorities)
var newPrefs = prefMan.GetPreferences(user); {
var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter; if (priority == JobPriority.Never)
Assert.That(newProf.AntagPreferences.Contains(id), Is.EqualTo(value)); dictionary.Remove(job);
else
dictionary[job] = priority;
}
var newProfile = profile.WithJobPriorities(dictionary);
_modifiedProfiles.Add(user);
await Server.WaitPost(() => prefMan.SetProfile(user, 0, newProfile).Wait());
} }
} }

View File

@@ -2,10 +2,12 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Preferences.Managers;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Mind; using Content.Shared.Mind;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
using Content.Shared.Preferences;
using Robust.Client; using Robust.Client;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Exceptions; using Robust.Shared.Exceptions;
@@ -34,6 +36,9 @@ public sealed partial class TestPair : IAsyncDisposable
private async Task OnCleanDispose() private async Task OnCleanDispose()
{ {
await Server.WaitIdleAsync();
await Client.WaitIdleAsync();
await ResetModifiedPreferences();
await Server.RemoveAllDummySessions(); await Server.RemoveAllDummySessions();
if (TestMap != null) if (TestMap != null)
@@ -81,6 +86,16 @@ public sealed partial class TestPair : IAsyncDisposable
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool"); await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
} }
private async Task ResetModifiedPreferences()
{
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
foreach (var user in _modifiedProfiles)
{
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
}
_modifiedProfiles.Clear();
}
public async ValueTask CleanReturnAsync() public async ValueTask CleanReturnAsync()
{ {
if (State != PairState.InUse) if (State != PairState.InUse)

View File

@@ -26,6 +26,8 @@ public sealed partial class TestPair
public readonly List<string> TestHistory = new(); public readonly List<string> TestHistory = new();
public PoolSettings Settings = default!; public PoolSettings Settings = default!;
public TestMapData? TestMap; public TestMapData? TestMap;
private List<NetUserId> _modifiedProfiles = new();
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!; public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!; public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
@@ -37,9 +39,7 @@ public sealed partial class TestPair
client = Client; client = Client;
} }
public ICommonSession? Player => Client.User == null public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
? null
: Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User.Value);
public ContentPlayerData? PlayerData => Player?.Data.ContentData(); public ContentPlayerData? PlayerData => Player?.Data.ContentData();

View File

@@ -28,6 +28,7 @@ public static partial class PoolManager
(CCVars.EmergencyShuttleEnabled.Name, "false"), (CCVars.EmergencyShuttleEnabled.Name, "false"),
(CCVars.ProcgenPreload.Name, "false"), (CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"), (CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.Name, "false"),
(CVars.ReplayClientRecordingEnabled.Name, "false"), (CVars.ReplayClientRecordingEnabled.Name, "false"),
(CVars.ReplayServerRecordingEnabled.Name, "false"), (CVars.ReplayServerRecordingEnabled.Name, "false"),
(CCVars.GameDummyTicker.Name, "true"), (CCVars.GameDummyTicker.Name, "true"),

View File

@@ -340,6 +340,7 @@ namespace Content.IntegrationTests.Tests
"MapGrid", "MapGrid",
"Broadphase", "Broadphase",
"StationData", // errors when removed mid-round "StationData", // errors when removed mid-round
"StationJobs",
"Actor", // We aren't testing actor components, those need their player session set. "Actor", // We aren't testing actor components, those need their player session set.
"BlobFloorPlanBuilder", // Implodes if unconfigured. "BlobFloorPlanBuilder", // Implodes if unconfigured.
"DebrisFeaturePlacerController", // Above. "DebrisFeaturePlacerController", // Above.

View File

@@ -52,7 +52,7 @@ public sealed class AntagPreferenceTest
Assert.That(pool.Count, Is.EqualTo(0)); Assert.That(pool.Count, Is.EqualTo(0));
// Opt into the traitor role. // Opt into the traitor role.
await pair.SetAntagPref("Traitor", true); await pair.SetAntagPreference("Traitor", true);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True); Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True); Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
@@ -63,7 +63,7 @@ public sealed class AntagPreferenceTest
Assert.That(sessions.Count, Is.EqualTo(1)); Assert.That(sessions.Count, Is.EqualTo(1));
// opt back out // opt back out
await pair.SetAntagPref("Traitor", false); await pair.SetAntagPreference("Traitor", false);
Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True); Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True);
Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True); Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);

View File

@@ -62,8 +62,8 @@ public sealed class NukeOpsTest
await pair.RunTicksSync(5); await pair.RunTicksSync(5);
// Opt into the nukies role. // Opt into the nukies role.
await pair.SetAntagPref("NukeopsCommander", true); await pair.SetAntagPreference("NukeopsCommander", true);
await pair.SetAntagPref(dummies[1].UserId, "NukeopsMedic", true); await pair.SetAntagPreference( "NukeopsMedic", true, dummies[1].UserId);
// Initially, the players have no attached entities // Initially, the players have no attached entities
Assert.That(pair.Player?.AttachedEntity, Is.Null); Assert.That(pair.Player?.AttachedEntity, Is.Null);
@@ -236,8 +236,6 @@ public sealed class NukeOpsTest
} }
ticker.SetGamePreset((GamePresetPrototype?)null); ticker.SetGamePreset((GamePresetPrototype?)null);
await pair.SetAntagPref("NukeopsCommander", false);
await pair.SetAntagPref(dummies[1].UserId, "NukeopsMedic", false);
await pair.CleanReturnAsync(); await pair.CleanReturnAsync();
} }
} }

View File

@@ -245,22 +245,15 @@ namespace Content.IntegrationTests.Tests
// Test all availableJobs have spawnPoints // Test all availableJobs have spawnPoints
// This is done inside gamemap test because loading the map takes ages and we already have it. // This is done inside gamemap test because loading the map takes ages and we already have it.
var jobList = entManager.GetComponent<StationJobsComponent>(station).RoundStartJobList var comp = entManager.GetComponent<StationJobsComponent>(station);
.Where(x => x.Value != 0) var jobs = new HashSet<ProtoId<JobPrototype>>(comp.SetupAvailableJobs.Keys);
.Select(x => x.Key);
var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
.Where(spawnpoint => spawnpoint.SpawnType == SpawnPointType.Job)
.Select(spawnpoint => spawnpoint.Job.ID)
.Distinct();
List<string> missingSpawnPoints = new();
foreach (var spawnpoint in jobList.Except(spawnPoints))
{
if (protoManager.Index<JobPrototype>(spawnpoint).SetPreference)
missingSpawnPoints.Add(spawnpoint);
}
Assert.That(missingSpawnPoints, Has.Count.EqualTo(0), var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
$"There is no spawnpoint for {string.Join(", ", missingSpawnPoints)} on {mapProto}."); .Where(x => x.SpawnType == SpawnPointType.Job)
.Select(x => x.Job!.Value);
jobs.ExceptWith(spawnPoints);
Assert.That(jobs, Is.Empty,$"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
} }
try try

View File

@@ -0,0 +1,222 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.IntegrationTests.Pair;
using Content.Server.GameTicking;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Round;
[TestFixture]
public sealed class JobTest
{
private static ProtoId<JobPrototype> _passenger = "Passenger";
private static ProtoId<JobPrototype> _engineer = "StationEngineer";
private static ProtoId<JobPrototype> _captain = "Captain";
private static string _map = "JobTestMap";
[TestPrototypes]
public static string JobTestMap = @$"
- type: gameMap
id: {_map}
mapName: {_map}
mapPath: /Maps/Test/empty.yml
minPlayers: 0
stations:
Empty:
stationProto: StandardNanotrasenStation
components:
- type: StationNameSetup
mapNameTemplate: ""Empty""
- type: StationJobs
availableJobs:
{_passenger}: [ -1, -1 ]
{_engineer}: [ -1, -1 ]
{_captain}: [ 1, 1 ]
";
public void AssertJob(TestPair pair, ProtoId<JobPrototype> job, NetUserId? user = null, bool isAntag = false)
{
var jobSys = pair.Server.System<SharedJobSystem>();
var mindSys = pair.Server.System<MindSystem>();
var roleSys = pair.Server.System<RoleSystem>();
var ticker = pair.Server.System<GameTicker>();
user ??= pair.Client.User!.Value;
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
Assert.That(ticker.PlayerGameStatuses[user.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
var uid = pair.Server.PlayerMan.SessionsDict.GetValueOrDefault(user.Value)?.AttachedEntity;
Assert.That(pair.Server.EntMan.EntityExists(uid));
var mind = mindSys.GetMind(uid!.Value);
Assert.That(pair.Server.EntMan.EntityExists(mind));
Assert.That(jobSys.MindTryGetJobId(mind, out var actualJob));
Assert.That(actualJob, Is.EqualTo(job));
Assert.That(roleSys.MindIsAntagonist(mind), Is.EqualTo(isAntag));
}
/// <summary>
/// Simple test that checks that starting the round spawns the player into the test map as a passenger.
/// </summary>
[Test]
public async Task StartRoundTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
// Initially in the lobby
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
Assert.That(ticker.PlayerGameStatuses[pair.Client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
// Ready up and start the round
ticker.ToggleReadyAll(true);
Assert.That(ticker.PlayerGameStatuses[pair.Client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _passenger);
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
/// <summary>
/// Check that job preferences are respected.
/// </summary>
[Test]
public async Task JobPreferenceTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
await pair.SetJobPriorities((_passenger, JobPriority.Medium), (_engineer, JobPriority.High));
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _engineer);
await pair.Server.WaitPost(() => ticker.RestartRound());
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
await pair.SetJobPriorities((_passenger, JobPriority.High), (_engineer, JobPriority.Medium));
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _passenger);
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
/// <summary>
/// Check high priority jobs (e.g., captain) are selected before other roles, even if it means a player does not
/// get their preferred job.
/// </summary>
[Test]
public async Task JobWeightTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
var captain = pair.Server.ProtoMan.Index(_captain);
var engineer = pair.Server.ProtoMan.Index(_engineer);
var passenger = pair.Server.ProtoMan.Index(_passenger);
Assert.That(captain.Weight, Is.GreaterThan(engineer.Weight));
Assert.That(engineer.Weight, Is.EqualTo(passenger.Weight));
await pair.SetJobPriorities((_passenger, JobPriority.Medium), (_engineer, JobPriority.High), (_captain, JobPriority.Low));
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _captain);
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
/// <summary>
/// Check that jobs are preferentially given to players that have marked those jobs as higher priority.
/// </summary>
[Test]
public async Task JobPriorityTest()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
DummyTicker = false,
Connected = true,
InLobby = true
});
pair.Server.CfgMan.SetCVar(CCVars.GameMap, _map);
var ticker = pair.Server.System<GameTicker>();
Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
Assert.That(pair.Client.AttachedEntity, Is.Null);
await pair.Server.AddDummySessions(5);
await pair.RunTicksSync(5);
var engineers = pair.Server.PlayerMan.Sessions.Select(x => x.UserId).ToList();
var captain = engineers[3];
engineers.RemoveAt(3);
await pair.SetJobPriorities(captain, (_captain, JobPriority.High), (_engineer, JobPriority.Medium));
foreach (var engi in engineers)
{
await pair.SetJobPriorities(engi, (_captain, JobPriority.Medium), (_engineer, JobPriority.High));
}
ticker.ToggleReadyAll(true);
await pair.Server.WaitPost(() => ticker.StartRound());
await pair.RunTicksSync(10);
AssertJob(pair, _captain, captain);
Assert.Multiple(() =>
{
foreach (var engi in engineers)
{
AssertJob(pair, _engineer, engi);
}
});
await pair.Server.WaitPost(() => ticker.RestartRound());
await pair.CleanReturnAsync();
}
}

View File

@@ -7,7 +7,6 @@ using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -46,8 +45,6 @@ public sealed class StationJobsTest
stationProto: StandardNanotrasenStation stationProto: StandardNanotrasenStation
components: components:
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
TMime: [0, -1] TMime: [0, -1]
TAssistant: [-1, -1] TAssistant: [-1, -1]
@@ -164,7 +161,6 @@ public sealed class StationJobsTest
var server = pair.Server; var server = pair.Server;
var prototypeManager = server.ResolveDependency<IPrototypeManager>(); var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation"); var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager; var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>(); var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
@@ -215,6 +211,8 @@ public sealed class StationJobsTest
var server = pair.Server; var server = pair.Server;
var prototypeManager = server.ResolveDependency<IPrototypeManager>(); var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var name = compFact.GetComponentName<StationJobsComponent>();
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
@@ -233,11 +231,14 @@ public sealed class StationJobsTest
{ {
foreach (var (stationId, station) in gameMap.Stations) foreach (var (stationId, station) in gameMap.Stations)
{ {
if (!station.StationComponentOverrides.TryGetComponent("StationJobs", out var comp)) if (!station.StationComponentOverrides.TryGetComponent(name, out var comp))
continue; continue;
foreach (var (job, _) in ((StationJobsComponent) comp).SetupAvailableJobs) foreach (var (job, array) in ((StationJobsComponent) comp).SetupAvailableJobs)
{ {
Assert.That(array.Length, Is.EqualTo(2));
Assert.That(array[0] is -1 or >= 0);
Assert.That(array[1] is -1 or >= 0);
Assert.That(invalidJobs, Does.Not.Contain(job), $"Station {stationId} contains job prototype {job} which cannot be present roundstart."); Assert.That(invalidJobs, Does.Not.Contain(job), $"Station {stationId} contains job prototype {job} which cannot be present roundstart.");
} }
} }

View File

@@ -15,6 +15,7 @@ using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Traits;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -183,9 +184,9 @@ namespace Content.Server.Database
private static HumanoidCharacterProfile ConvertProfiles(Profile profile) private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
{ {
var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority); var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
var antags = profile.Antags.Select(a => a.AntagName); var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
var traits = profile.Traits.Select(t => t.TraitName); var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
var sex = Sex.Male; var sex = Sex.Male;
if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal)) if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))

View File

@@ -239,7 +239,7 @@ namespace Content.Server.GameTicking
HumanoidCharacterProfile profile; HumanoidCharacterProfile profile;
if (_prefsManager.TryGetCachedPreferences(userId, out var preferences)) if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))
{ {
profile = (HumanoidCharacterProfile) preferences.GetProfile(preferences.SelectedCharacterIndex); profile = (HumanoidCharacterProfile) preferences.SelectedCharacter;
} }
else else
{ {

View File

@@ -305,11 +305,7 @@ namespace Content.Server.Preferences.Managers
return usernames return usernames
.Select(p => (_cachedPlayerPrefs[p].Prefs, p)) .Select(p => (_cachedPlayerPrefs[p].Prefs, p))
.Where(p => p.Prefs != null) .Where(p => p.Prefs != null)
.Select(p => .Select(p => new KeyValuePair<NetUserId, ICharacterProfile>(p.p, p.Prefs!.SelectedCharacter));
{
var idx = p.Prefs!.SelectedCharacterIndex;
return new KeyValuePair<NetUserId, ICharacterProfile>(p.p, p.Prefs!.GetProfile(idx));
});
} }
internal static bool ShouldStorePrefs(LoginType loginType) internal static bool ShouldStorePrefs(LoginType loginType)

View File

@@ -6,11 +6,8 @@ namespace Content.Server.Spawners.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class SpawnPointComponent : Component, ISpawnPoint public sealed partial class SpawnPointComponent : Component, ISpawnPoint
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("job_id")] [DataField("job_id")]
private string? _jobId; public ProtoId<JobPrototype>? Job;
/// <summary> /// <summary>
/// The type of spawn point /// The type of spawn point
@@ -18,11 +15,9 @@ public sealed partial class SpawnPointComponent : Component, ISpawnPoint
[DataField("spawn_type"), ViewVariables(VVAccess.ReadWrite)] [DataField("spawn_type"), ViewVariables(VVAccess.ReadWrite)]
public SpawnPointType SpawnType { get; set; } = SpawnPointType.Unset; public SpawnPointType SpawnType { get; set; } = SpawnPointType.Unset;
public JobPrototype? Job => string.IsNullOrEmpty(_jobId) ? null : _prototypeManager.Index<JobPrototype>(_jobId);
public override string ToString() public override string ToString()
{ {
return $"{_jobId} {SpawnType}"; return $"{Job} {SpawnType}";
} }
} }

View File

@@ -39,7 +39,7 @@ public sealed class SpawnPointSystem : EntitySystem
if (_gameTicker.RunLevel != GameRunLevel.InRound && if (_gameTicker.RunLevel != GameRunLevel.InRound &&
spawnPoint.SpawnType == SpawnPointType.Job && spawnPoint.SpawnType == SpawnPointType.Job &&
(args.Job == null || spawnPoint.Job?.ID == args.Job.Prototype)) (args.Job == null || spawnPoint.Job == args.Job.Prototype))
{ {
possiblePositions.Add(xform.Coordinates); possiblePositions.Add(xform.Coordinates);
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Station.Systems; using System.Linq;
using Content.Server.Station.Systems;
using Content.Shared.Roles; using Content.Shared.Roles;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Network; using Robust.Shared.Network;
@@ -14,25 +15,21 @@ namespace Content.Server.Station.Components;
[RegisterComponent, Access(typeof(StationJobsSystem)), PublicAPI] [RegisterComponent, Access(typeof(StationJobsSystem)), PublicAPI]
public sealed partial class StationJobsComponent : Component public sealed partial class StationJobsComponent : Component
{ {
/// <summary>
/// Total *round-start* jobs at station start.
/// </summary>
[DataField("roundStartTotalJobs")] public int RoundStartTotalJobs;
/// <summary> /// <summary>
/// Total *mid-round* jobs at station start. /// Total *mid-round* jobs at station start.
/// This is inferred automatically from <see cref="SetupAvailableJobs"/>.
/// </summary> /// </summary>
[DataField("midRoundTotalJobs")] public int MidRoundTotalJobs; [ViewVariables] public int MidRoundTotalJobs;
/// <summary> /// <summary>
/// Current total jobs. /// Current total jobs.
/// </summary> /// </summary>
[DataField("totalJobs")] public int TotalJobs; [DataField] public int TotalJobs;
/// <summary> /// <summary>
/// Station is running on extended access. /// Station is running on extended access.
/// </summary> /// </summary>
[DataField("extendedAccess")] public bool ExtendedAccess; [DataField] public bool ExtendedAccess;
/// <summary> /// <summary>
/// If there are less than or equal this amount of players in the game at round start, /// If there are less than or equal this amount of players in the game at round start,
@@ -41,7 +38,7 @@ public sealed partial class StationJobsComponent : Component
/// <remarks> /// <remarks>
/// Set to -1 to disable extended access. /// Set to -1 to disable extended access.
/// </remarks> /// </remarks>
[DataField("extendedAccessThreshold")] [DataField]
public int ExtendedAccessThreshold { get; set; } = 15; public int ExtendedAccessThreshold { get; set; } = 15;
/// <summary> /// <summary>
@@ -54,28 +51,20 @@ public sealed partial class StationJobsComponent : Component
public float? PercentJobsRemaining => MidRoundTotalJobs > 0 ? TotalJobs / (float) MidRoundTotalJobs : null; public float? PercentJobsRemaining => MidRoundTotalJobs > 0 ? TotalJobs / (float) MidRoundTotalJobs : null;
/// <summary> /// <summary>
/// The current list of jobs. /// The current list of jobs of available jobs. Null implies that is no limit.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This should not be mutated or used directly unless you really know what you're doing, go through StationJobsSystem. /// This should not be mutated or used directly unless you really know what you're doing, go through StationJobsSystem.
/// </remarks> /// </remarks>
[DataField("jobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<uint?, JobPrototype>))] [DataField]
public Dictionary<string, uint?> JobList = new(); public Dictionary<ProtoId<JobPrototype>, int?> JobList = new();
/// <summary>
/// The round-start list of jobs.
/// </summary>
/// <remarks>
/// This should not be mutated, ever.
/// </remarks>
[DataField("roundStartJobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<uint?, JobPrototype>))]
public Dictionary<string, uint?> RoundStartJobList = new();
/// <summary> /// <summary>
/// Overflow jobs that round-start can spawn infinitely many of. /// Overflow jobs that round-start can spawn infinitely many of.
/// This is inferred automatically from <see cref="SetupAvailableJobs"/>.
/// </summary> /// </summary>
[DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))] [ViewVariables]
public HashSet<string> OverflowJobs = new(); public IReadOnlySet<ProtoId<JobPrototype>> OverflowJobs = default!;
/// <summary> /// <summary>
/// A dictionary relating a NetUserId to the jobs they have on station. /// A dictionary relating a NetUserId to the jobs they have on station.
@@ -84,7 +73,10 @@ public sealed partial class StationJobsComponent : Component
[DataField] [DataField]
public Dictionary<NetUserId, List<ProtoId<JobPrototype>>> PlayerJobs = new(); public Dictionary<NetUserId, List<ProtoId<JobPrototype>>> PlayerJobs = new();
[DataField("availableJobs", required: true, /// <summary>
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))] /// Mapping of jobs to an int[2] array that specifies jobs available at round start, and midround.
public Dictionary<string, List<int?>> SetupAvailableJobs = default!; /// Negative values implies that there is no limit.
/// </summary>
[DataField("availableJobs", required: true)]
public Dictionary<ProtoId<JobPrototype>, int[]> SetupAvailableJobs = default!;
} }

View File

@@ -52,23 +52,23 @@ public sealed partial class StationJobsSystem
/// as there may end up being more round-start slots than available slots, which can cause weird behavior. /// as there may end up being more round-start slots than available slots, which can cause weird behavior.
/// A warning to all who enter ye cursed lands: This function is long and mildly incomprehensible. Best used without touching. /// A warning to all who enter ye cursed lands: This function is long and mildly incomprehensible. Best used without touching.
/// </remarks> /// </remarks>
public Dictionary<NetUserId, (string?, EntityUid)> AssignJobs(Dictionary<NetUserId, HumanoidCharacterProfile> profiles, IReadOnlyList<EntityUid> stations, bool useRoundStartJobs = true) public Dictionary<NetUserId, (ProtoId<JobPrototype>?, EntityUid)> AssignJobs(Dictionary<NetUserId, HumanoidCharacterProfile> profiles, IReadOnlyList<EntityUid> stations, bool useRoundStartJobs = true)
{ {
DebugTools.Assert(stations.Count > 0); DebugTools.Assert(stations.Count > 0);
InitializeRoundStart(); InitializeRoundStart();
if (profiles.Count == 0) if (profiles.Count == 0)
return new Dictionary<NetUserId, (string?, EntityUid)>(); return new();
// We need to modify this collection later, so make a copy of it. // We need to modify this collection later, so make a copy of it.
profiles = profiles.ShallowClone(); profiles = profiles.ShallowClone();
// Player <-> (job, station) // Player <-> (job, station)
var assigned = new Dictionary<NetUserId, (string?, EntityUid)>(profiles.Count); var assigned = new Dictionary<NetUserId, (ProtoId<JobPrototype>?, EntityUid)>(profiles.Count);
// The jobs left on the stations. This collection is modified as jobs are assigned to track what's available. // The jobs left on the stations. This collection is modified as jobs are assigned to track what's available.
var stationJobs = new Dictionary<EntityUid, Dictionary<string, uint?>>(); var stationJobs = new Dictionary<EntityUid, Dictionary<ProtoId<JobPrototype>, int?>>();
foreach (var station in stations) foreach (var station in stations)
{ {
if (useRoundStartJobs) if (useRoundStartJobs)
@@ -83,15 +83,15 @@ public sealed partial class StationJobsSystem
// We reuse this collection. It tracks what jobs we're currently trying to select players for. // We reuse this collection. It tracks what jobs we're currently trying to select players for.
var currentlySelectingJobs = new Dictionary<EntityUid, Dictionary<string, uint?>>(stations.Count); var currentlySelectingJobs = new Dictionary<EntityUid, Dictionary<ProtoId<JobPrototype>, int?>>(stations.Count);
foreach (var station in stations) foreach (var station in stations)
{ {
currentlySelectingJobs.Add(station, new Dictionary<string, uint?>()); currentlySelectingJobs.Add(station, new Dictionary<ProtoId<JobPrototype>, int?>());
} }
// And these. // And these.
// Tracks what players are available for a given job in the current iteration of selection. // Tracks what players are available for a given job in the current iteration of selection.
var jobPlayerOptions = new Dictionary<string, HashSet<NetUserId>>(); var jobPlayerOptions = new Dictionary<ProtoId<JobPrototype>, HashSet<NetUserId>>();
// Tracks the total number of slots for the given stations in the current iteration of selection. // Tracks the total number of slots for the given stations in the current iteration of selection.
var stationTotalSlots = new Dictionary<EntityUid, int>(stations.Count); var stationTotalSlots = new Dictionary<EntityUid, int>(stations.Count);
// The share of the players each station gets in the current iteration of job selection. // The share of the players each station gets in the current iteration of job selection.
@@ -112,7 +112,7 @@ public sealed partial class StationJobsSystem
var optionsRemaining = 0; var optionsRemaining = 0;
// Assigns a player to the given station, updating all the bookkeeping while at it. // Assigns a player to the given station, updating all the bookkeeping while at it.
void AssignPlayer(NetUserId player, string job, EntityUid station) void AssignPlayer(NetUserId player, ProtoId<JobPrototype> job, EntityUid station)
{ {
// Remove the player from all possible jobs as that's faster than actually checking what they have selected. // Remove the player from all possible jobs as that's faster than actually checking what they have selected.
foreach (var (k, players) in jobPlayerOptions) foreach (var (k, players) in jobPlayerOptions)
@@ -273,8 +273,11 @@ public sealed partial class StationJobsSystem
/// <param name="allPlayersToAssign">All players that might need an overflow assigned.</param> /// <param name="allPlayersToAssign">All players that might need an overflow assigned.</param>
/// <param name="profiles">Player character profiles.</param> /// <param name="profiles">Player character profiles.</param>
/// <param name="stations">The stations to consider for spawn location.</param> /// <param name="stations">The stations to consider for spawn location.</param>
public void AssignOverflowJobs(ref Dictionary<NetUserId, (string?, EntityUid)> assignedJobs, public void AssignOverflowJobs(
IEnumerable<NetUserId> allPlayersToAssign, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles, IReadOnlyList<EntityUid> stations) ref Dictionary<NetUserId, (ProtoId<JobPrototype>?, EntityUid)> assignedJobs,
IEnumerable<NetUserId> allPlayersToAssign,
IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles,
IReadOnlyList<EntityUid> stations)
{ {
var givenStations = stations.ToList(); var givenStations = stations.ToList();
if (givenStations.Count == 0) if (givenStations.Count == 0)

View File

@@ -3,6 +3,7 @@ using System.Linq;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -31,12 +32,25 @@ public sealed partial class StationJobsSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialized); SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialized);
SubscribeLocalEvent<StationJobsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<StationJobsComponent, StationRenamedEvent>(OnStationRenamed); SubscribeLocalEvent<StationJobsComponent, StationRenamedEvent>(OnStationRenamed);
SubscribeLocalEvent<StationJobsComponent, ComponentShutdown>(OnStationDeletion); SubscribeLocalEvent<StationJobsComponent, ComponentShutdown>(OnStationDeletion);
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby); SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
Subs.CVar(_configurationManager, CCVars.GameDisallowLateJoins, _ => UpdateJobsAvailable(), true); Subs.CVar(_configurationManager, CCVars.GameDisallowLateJoins, _ => UpdateJobsAvailable(), true);
} }
private void OnInit(Entity<StationJobsComponent> ent, ref ComponentInit args)
{
ent.Comp.MidRoundTotalJobs = ent.Comp.SetupAvailableJobs.Values
.Select(x => Math.Max(x[1], 0))
.Sum();
ent.Comp.OverflowJobs = ent.Comp.SetupAvailableJobs
.Where(x => x.Value[0] < 0)
.Select(x => x.Key)
.ToHashSet();
}
public override void Update(float _) public override void Update(float _)
{ {
if (_availableJobsDirty) if (_availableJobsDirty)
@@ -57,28 +71,11 @@ public sealed partial class StationJobsSystem : EntitySystem
if (!TryComp<StationJobsComponent>(msg.Station, out var stationJobs)) if (!TryComp<StationJobsComponent>(msg.Station, out var stationJobs))
return; return;
var mapJobList = stationJobs.SetupAvailableJobs; stationJobs.JobList = stationJobs.SetupAvailableJobs.ToDictionary(
x => x.Key,
x=> (int?)(x.Value[1] < 0 ? null : x.Value[1]));
stationJobs.RoundStartTotalJobs = mapJobList.Values.Where(x => x[0] is not null && x[0] > 0).Sum(x => x[0]!.Value); stationJobs.TotalJobs = stationJobs.JobList.Values.Select(x => x ?? 0).Sum();
stationJobs.MidRoundTotalJobs = mapJobList.Values.Where(x => x[1] is not null && x[1] > 0).Sum(x => x[1]!.Value);
stationJobs.TotalJobs = stationJobs.MidRoundTotalJobs;
stationJobs.JobList = mapJobList.ToDictionary(x => x.Key, x =>
{
if (x.Value[1] <= -1)
return null;
return (uint?) x.Value[1];
});
stationJobs.RoundStartJobList = mapJobList.ToDictionary(x => x.Key, x =>
{
if (x.Value[0] <= -1)
return null;
return (uint?) x.Value[0];
});
stationJobs.OverflowJobs = stationJobs.OverflowJobs.ToHashSet();
UpdateJobsAvailable(); UpdateJobsAvailable();
} }
@@ -141,7 +138,11 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
/// <returns>Whether or not slot adjustment was a success.</returns> /// <returns>Whether or not slot adjustment was a success.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
public bool TryAdjustJobSlot(EntityUid station, string jobPrototypeId, int amount, bool createSlot = false, bool clamp = false, public bool TryAdjustJobSlot(EntityUid station,
string jobPrototypeId,
int amount,
bool createSlot = false,
bool clamp = false,
StationJobsComponent? stationJobs = null) StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
@@ -156,7 +157,11 @@ public sealed partial class StationJobsSystem : EntitySystem
// - Return false when you remove from a job that doesn't exist. // - Return false when you remove from a job that doesn't exist.
// - Return false when you remove and exceed the number of slots available. // - Return false when you remove and exceed the number of slots available.
// And additionally, if adding would add a job not previously on the manifest when createSlot is false, return false and do nothing. // And additionally, if adding would add a job not previously on the manifest when createSlot is false, return false and do nothing.
switch (jobList.ContainsKey(jobPrototypeId))
if (amount == 0)
return true;
switch (jobList.TryGetValue(jobPrototypeId, out var available))
{ {
case false when amount < 0: case false when amount < 0:
return false; return false;
@@ -164,31 +169,20 @@ public sealed partial class StationJobsSystem : EntitySystem
if (!createSlot) if (!createSlot)
return false; return false;
stationJobs.TotalJobs += amount; stationJobs.TotalJobs += amount;
jobList[jobPrototypeId] = (uint?)amount; jobList[jobPrototypeId] = amount;
UpdateJobsAvailable(); UpdateJobsAvailable();
return true; return true;
case true: case true:
// Job is unlimited so just say we adjusted it and do nothing. // Job is unlimited so just say we adjusted it and do nothing.
if (jobList[jobPrototypeId] == null) if (available is not {} avail)
return true; return true;
// Would remove more jobs than we have available. // Would remove more jobs than we have available.
if (amount < 0 && (jobList[jobPrototypeId] + amount < 0 && !clamp)) if (available + amount < 0 && !clamp)
return false; return false;
stationJobs.TotalJobs += amount; jobList[jobPrototypeId] = Math.Max(avail + amount, 0);
stationJobs.TotalJobs = jobList.Values.Select(x => x ?? 0).Sum();
//C# type handling moment
if (amount > 0)
jobList[jobPrototypeId] += (uint)amount;
else
{
if ((int)jobList[jobPrototypeId]!.Value - Math.Abs(amount) <= 0)
jobList[jobPrototypeId] = 0;
else
jobList[jobPrototypeId] -= (uint) Math.Abs(amount);
}
UpdateJobsAvailable(); UpdateJobsAvailable();
return true; return true;
} }
@@ -239,7 +233,10 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
/// <returns>Whether or not setting the value succeeded.</returns> /// <returns>Whether or not setting the value succeeded.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
public bool TrySetJobSlot(EntityUid station, string jobPrototypeId, int amount, bool createSlot = false, public bool TrySetJobSlot(EntityUid station,
string jobPrototypeId,
int amount,
bool createSlot = false,
StationJobsComponent? stationJobs = null) StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
@@ -255,13 +252,13 @@ public sealed partial class StationJobsSystem : EntitySystem
if (!createSlot) if (!createSlot)
return false; return false;
stationJobs.TotalJobs += amount; stationJobs.TotalJobs += amount;
jobList[jobPrototypeId] = (uint?)amount; jobList[jobPrototypeId] = amount;
UpdateJobsAvailable(); UpdateJobsAvailable();
return true; return true;
case true: case true:
stationJobs.TotalJobs += amount - (int) (jobList[jobPrototypeId] ?? 0); stationJobs.TotalJobs += amount - (jobList[jobPrototypeId] ?? 0);
jobList[jobPrototypeId] = (uint)amount; jobList[jobPrototypeId] = amount;
UpdateJobsAvailable(); UpdateJobsAvailable();
return true; return true;
} }
@@ -289,8 +286,8 @@ public sealed partial class StationJobsSystem : EntitySystem
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
// Subtract out the job we're fixing to make have unlimited slots. // Subtract out the job we're fixing to make have unlimited slots.
if (stationJobs.JobList.ContainsKey(jobPrototypeId) && stationJobs.JobList[jobPrototypeId] != null) if (stationJobs.JobList.TryGetValue(jobPrototypeId, out var existing))
stationJobs.TotalJobs -= (int)stationJobs.JobList[jobPrototypeId]!.Value; stationJobs.TotalJobs -= existing ?? 0;
stationJobs.JobList[jobPrototypeId] = null; stationJobs.JobList[jobPrototypeId] = null;
@@ -319,8 +316,7 @@ public sealed partial class StationJobsSystem : EntitySystem
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
var res = stationJobs.JobList.TryGetValue(jobPrototypeId, out var job) && job == null; return stationJobs.JobList.TryGetValue(jobPrototypeId, out var job) && job == null;
return res;
} }
/// <inheritdoc cref="TryGetJobSlot(Robust.Shared.GameObjects.EntityUid,string,out System.Nullable{uint},Content.Server.Station.Components.StationJobsComponent?)"/> /// <inheritdoc cref="TryGetJobSlot(Robust.Shared.GameObjects.EntityUid,string,out System.Nullable{uint},Content.Server.Station.Components.StationJobsComponent?)"/>
@@ -328,7 +324,7 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="job">Job to get slot info for.</param> /// <param name="job">Job to get slot info for.</param>
/// <param name="slots">The number of slots remaining. Null if infinite.</param> /// <param name="slots">The number of slots remaining. Null if infinite.</param>
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
public bool TryGetJobSlot(EntityUid station, JobPrototype job, out uint? slots, StationJobsComponent? stationJobs = null) public bool TryGetJobSlot(EntityUid station, JobPrototype job, out int? slots, StationJobsComponent? stationJobs = null)
{ {
return TryGetJobSlot(station, job.ID, out slots, stationJobs); return TryGetJobSlot(station, job.ID, out slots, stationJobs);
} }
@@ -343,21 +339,12 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <returns>Whether or not the slot exists.</returns> /// <returns>Whether or not the slot exists.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
/// <remarks>slots will be null if the slot doesn't exist, as well, so make sure to check the return value.</remarks> /// <remarks>slots will be null if the slot doesn't exist, as well, so make sure to check the return value.</remarks>
public bool TryGetJobSlot(EntityUid station, string jobPrototypeId, out uint? slots, StationJobsComponent? stationJobs = null) public bool TryGetJobSlot(EntityUid station, string jobPrototypeId, out int? slots, StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
if (stationJobs.JobList.TryGetValue(jobPrototypeId, out var job)) return stationJobs.JobList.TryGetValue(jobPrototypeId, out slots);
{
slots = job;
return true;
}
else // Else if slot isn't present return null.
{
slots = null;
return false;
}
} }
/// <summary> /// <summary>
@@ -367,12 +354,14 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
/// <returns>Set containing all jobs available.</returns> /// <returns>Set containing all jobs available.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
public IReadOnlySet<string> GetAvailableJobs(EntityUid station, StationJobsComponent? stationJobs = null) public IEnumerable<ProtoId<JobPrototype>> GetAvailableJobs(EntityUid station, StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
return stationJobs.JobList.Where(x => x.Value != 0).Select(x => x.Key).ToHashSet(); return stationJobs.JobList
.Where(x => x.Value != 0)
.Select(x => x.Key);
} }
/// <summary> /// <summary>
@@ -382,12 +371,12 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
/// <returns>Set containing all overflow jobs available.</returns> /// <returns>Set containing all overflow jobs available.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
public IReadOnlySet<string> GetOverflowJobs(EntityUid station, StationJobsComponent? stationJobs = null) public IReadOnlySet<ProtoId<JobPrototype>> GetOverflowJobs(EntityUid station, StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
return stationJobs.OverflowJobs.ToHashSet(); return stationJobs.OverflowJobs;
} }
/// <summary> /// <summary>
@@ -397,7 +386,7 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
/// <returns>List of all jobs on the station.</returns> /// <returns>List of all jobs on the station.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
public IReadOnlyDictionary<string, uint?> GetJobs(EntityUid station, StationJobsComponent? stationJobs = null) public IReadOnlyDictionary<ProtoId<JobPrototype>, int?> GetJobs(EntityUid station, StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
@@ -412,12 +401,14 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param> /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
/// <returns>List of all round-start jobs.</returns> /// <returns>List of all round-start jobs.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception> /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
public IReadOnlyDictionary<string, uint?> GetRoundStartJobs(EntityUid station, StationJobsComponent? stationJobs = null) public Dictionary<ProtoId<JobPrototype>, int?> GetRoundStartJobs(EntityUid station, StationJobsComponent? stationJobs = null)
{ {
if (!Resolve(station, ref stationJobs)) if (!Resolve(station, ref stationJobs))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
return stationJobs.RoundStartJobList; return stationJobs.SetupAvailableJobs.ToDictionary(
x => x.Key,
x=> (int?)(x.Value[0] < 0 ? null : x.Value[0]));
} }
/// <summary> /// <summary>
@@ -428,13 +419,13 @@ public sealed partial class StationJobsSystem : EntitySystem
/// <param name="pickOverflows">Whether or not to pick from the overflow list.</param> /// <param name="pickOverflows">Whether or not to pick from the overflow list.</param>
/// <param name="disallowedJobs">A set of disallowed jobs, if any.</param> /// <param name="disallowedJobs">A set of disallowed jobs, if any.</param>
/// <returns>The selected job, if any.</returns> /// <returns>The selected job, if any.</returns>
public string? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary<string, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet<ProtoId<JobPrototype>>? disallowedJobs = null) public ProtoId<JobPrototype>? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet<ProtoId<JobPrototype>>? disallowedJobs = null)
{ {
if (station == EntityUid.Invalid) if (station == EntityUid.Invalid)
return null; return null;
var available = GetAvailableJobs(station); var available = GetAvailableJobs(station);
bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId) bool TryPick(JobPriority priority, [NotNullWhen(true)] out ProtoId<JobPrototype>? jobId)
{ {
var filtered = jobPriorities var filtered = jobPriorities
.Where(p => .Where(p =>
@@ -474,7 +465,10 @@ public sealed partial class StationJobsSystem : EntitySystem
return null; return null;
var overflows = GetOverflowJobs(station); var overflows = GetOverflowJobs(station);
return overflows.Count != 0 ? _random.Pick(overflows) : null; if (overflows.Count == 0)
return null;
return _random.Pick(overflows);
} }
#endregion Public API #endregion Public API
@@ -483,7 +477,7 @@ public sealed partial class StationJobsSystem : EntitySystem
private bool _availableJobsDirty; private bool _availableJobsDirty;
private TickerJobsAvailableEvent _cachedAvailableJobs = new (new Dictionary<NetEntity, string>(), new Dictionary<NetEntity, Dictionary<string, uint?>>()); private TickerJobsAvailableEvent _cachedAvailableJobs = new(new(), new());
/// <summary> /// <summary>
/// Assembles an event from the current available-to-play jobs. /// Assembles an event from the current available-to-play jobs.
@@ -494,9 +488,9 @@ public sealed partial class StationJobsSystem : EntitySystem
{ {
// If late join is disallowed, return no available jobs. // If late join is disallowed, return no available jobs.
if (_gameTicker.DisallowLateJoin) if (_gameTicker.DisallowLateJoin)
return new TickerJobsAvailableEvent(new Dictionary<NetEntity, string>(), new Dictionary<NetEntity, Dictionary<string, uint?>>()); return new TickerJobsAvailableEvent(new(), new());
var jobs = new Dictionary<NetEntity, Dictionary<string, uint?>>(); var jobs = new Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>>();
var stationNames = new Dictionary<NetEntity, string>(); var stationNames = new Dictionary<NetEntity, string>();
var query = EntityQueryEnumerator<StationJobsComponent>(); var query = EntityQueryEnumerator<StationJobsComponent>();

View File

@@ -1,5 +1,6 @@
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays; using Robust.Shared.Replays;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Mapping;
@@ -128,19 +129,17 @@ namespace Content.Shared.GameTicking
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class TickerJobsAvailableEvent : EntityEventArgs public sealed class TickerJobsAvailableEvent(
Dictionary<NetEntity, string> stationNames,
Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> jobsAvailableByStation)
: EntityEventArgs
{ {
/// <summary> /// <summary>
/// The Status of the Player in the lobby (ready, observer, ...) /// The Status of the Player in the lobby (ready, observer, ...)
/// </summary> /// </summary>
public Dictionary<NetEntity, Dictionary<string, uint?>> JobsAvailableByStation { get; } public Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailableByStation { get; } = jobsAvailableByStation;
public Dictionary<NetEntity, string> StationNames { get; }
public TickerJobsAvailableEvent(Dictionary<NetEntity, string> stationNames, Dictionary<NetEntity, Dictionary<string, uint?>> jobsAvailableByStation) public Dictionary<NetEntity, string> StationNames { get; } = stationNames;
{
StationNames = stationNames;
JobsAvailableByStation = jobsAvailableByStation;
}
} }
[Serializable, NetSerializable, DataDefinition] [Serializable, NetSerializable, DataDefinition]

View File

@@ -35,7 +35,7 @@ namespace Content.Shared.Preferences
/// Job preferences for initial spawn. /// Job preferences for initial spawn.
/// </summary> /// </summary>
[DataField] [DataField]
private Dictionary<string, JobPriority> _jobPriorities = new() private Dictionary<ProtoId<JobPrototype>, JobPriority> _jobPriorities = new()
{ {
{ {
SharedGameTicker.FallbackOverflowJob, JobPriority.High SharedGameTicker.FallbackOverflowJob, JobPriority.High
@@ -46,13 +46,13 @@ namespace Content.Shared.Preferences
/// Antags we have opted in to. /// Antags we have opted in to.
/// </summary> /// </summary>
[DataField] [DataField]
private HashSet<string> _antagPreferences = new(); private HashSet<ProtoId<AntagPrototype>> _antagPreferences = new();
/// <summary> /// <summary>
/// Enabled traits. /// Enabled traits.
/// </summary> /// </summary>
[DataField] [DataField]
private HashSet<string> _traitPreferences = new(); private HashSet<ProtoId<TraitPrototype>> _traitPreferences = new();
/// <summary> /// <summary>
/// <see cref="_loadouts"/> /// <see cref="_loadouts"/>
@@ -75,7 +75,7 @@ namespace Content.Shared.Preferences
/// Associated <see cref="SpeciesPrototype"/> for this profile. /// Associated <see cref="SpeciesPrototype"/> for this profile.
/// </summary> /// </summary>
[DataField] [DataField]
public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies; public ProtoId<SpeciesPrototype> Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
[DataField] [DataField]
public int Age { get; set; } = 18; public int Age { get; set; } = 18;
@@ -106,17 +106,17 @@ namespace Content.Shared.Preferences
/// <summary> /// <summary>
/// <see cref="_jobPriorities"/> /// <see cref="_jobPriorities"/>
/// </summary> /// </summary>
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities; public IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> JobPriorities => _jobPriorities;
/// <summary> /// <summary>
/// <see cref="_antagPreferences"/> /// <see cref="_antagPreferences"/>
/// </summary> /// </summary>
public IReadOnlySet<string> AntagPreferences => _antagPreferences; public IReadOnlySet<ProtoId<AntagPrototype>> AntagPreferences => _antagPreferences;
/// <summary> /// <summary>
/// <see cref="_traitPreferences"/> /// <see cref="_traitPreferences"/>
/// </summary> /// </summary>
public IReadOnlySet<string> TraitPreferences => _traitPreferences; public IReadOnlySet<ProtoId<TraitPrototype>> TraitPreferences => _traitPreferences;
/// <summary> /// <summary>
/// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby. /// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
@@ -134,10 +134,10 @@ namespace Content.Shared.Preferences
Gender gender, Gender gender,
HumanoidCharacterAppearance appearance, HumanoidCharacterAppearance appearance,
SpawnPriorityPreference spawnPriority, SpawnPriorityPreference spawnPriority,
Dictionary<string, JobPriority> jobPriorities, Dictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable, PreferenceUnavailableMode preferenceUnavailable,
HashSet<string> antagPreferences, HashSet<ProtoId<AntagPrototype>> antagPreferences,
HashSet<string> traitPreferences, HashSet<ProtoId<TraitPrototype>> traitPreferences,
Dictionary<string, RoleLoadout> loadouts) Dictionary<string, RoleLoadout> loadouts)
{ {
Name = name; Name = name;
@@ -153,6 +153,20 @@ namespace Content.Shared.Preferences
_antagPreferences = antagPreferences; _antagPreferences = antagPreferences;
_traitPreferences = traitPreferences; _traitPreferences = traitPreferences;
_loadouts = loadouts; _loadouts = loadouts;
var hasHighPrority = false;
foreach (var (key, value) in _jobPriorities)
{
if (value == JobPriority.Never)
_jobPriorities.Remove(key);
else if (value != JobPriority.High)
continue;
if (hasHighPrority)
_jobPriorities[key] = JobPriority.Medium;
hasHighPrority = true;
}
} }
/// <summary>Copy constructor</summary> /// <summary>Copy constructor</summary>
@@ -165,10 +179,10 @@ namespace Content.Shared.Preferences
other.Gender, other.Gender,
other.Appearance.Clone(), other.Appearance.Clone(),
other.SpawnPriority, other.SpawnPriority,
new Dictionary<string, JobPriority>(other.JobPriorities), new Dictionary<ProtoId<JobPrototype>, JobPriority>(other.JobPriorities),
other.PreferenceUnavailable, other.PreferenceUnavailable,
new HashSet<string>(other.AntagPreferences), new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
new HashSet<string>(other.TraitPreferences), new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
new Dictionary<string, RoleLoadout>(other.Loadouts)) new Dictionary<string, RoleLoadout>(other.Loadouts))
{ {
} }
@@ -289,21 +303,48 @@ namespace Content.Shared.Preferences
return new(this) { SpawnPriority = spawnPriority }; return new(this) { SpawnPriority = spawnPriority };
} }
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities) public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<ProtoId<JobPrototype>, JobPriority>> jobPriorities)
{ {
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(jobPriorities);
var hasHighPrority = false;
foreach (var (key, value) in dictionary)
{
if (value == JobPriority.Never)
dictionary.Remove(key);
else if (value != JobPriority.High)
continue;
if (hasHighPrority)
dictionary[key] = JobPriority.Medium;
hasHighPrority = true;
}
return new(this) return new(this)
{ {
_jobPriorities = new Dictionary<string, JobPriority>(jobPriorities), _jobPriorities = dictionary
}; };
} }
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority) public HumanoidCharacterProfile WithJobPriority(ProtoId<JobPrototype> jobId, JobPriority priority)
{ {
var dictionary = new Dictionary<string, JobPriority>(_jobPriorities); var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(_jobPriorities);
if (priority == JobPriority.Never) if (priority == JobPriority.Never)
{ {
dictionary.Remove(jobId); dictionary.Remove(jobId);
} }
else if (priority == JobPriority.High)
{
// There can only ever be one high priority job.
foreach (var (job, value) in dictionary)
{
if (value == JobPriority.High)
dictionary[job] = JobPriority.Medium;
}
dictionary[jobId] = priority;
}
else else
{ {
dictionary[jobId] = priority; dictionary[jobId] = priority;
@@ -320,17 +361,17 @@ namespace Content.Shared.Preferences
return new(this) { PreferenceUnavailable = mode }; return new(this) { PreferenceUnavailable = mode };
} }
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences) public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<ProtoId<AntagPrototype>> antagPreferences)
{ {
return new(this) return new(this)
{ {
_antagPreferences = new HashSet<string>(antagPreferences), _antagPreferences = new (antagPreferences),
}; };
} }
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref) public HumanoidCharacterProfile WithAntagPreference(ProtoId<AntagPrototype> antagId, bool pref)
{ {
var list = new HashSet<string>(_antagPreferences); var list = new HashSet<ProtoId<AntagPrototype>>(_antagPreferences);
if (pref) if (pref)
{ {
list.Add(antagId); list.Add(antagId);
@@ -346,16 +387,16 @@ namespace Content.Shared.Preferences
}; };
} }
public HumanoidCharacterProfile WithTraitPreference(string traitId, string? categoryId, bool pref) public HumanoidCharacterProfile WithTraitPreference(ProtoId<TraitPrototype> traitId, string? categoryId, bool pref)
{ {
var prototypeManager = IoCManager.Resolve<IPrototypeManager>(); var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var traitProto = prototypeManager.Index<TraitPrototype>(traitId); var traitProto = prototypeManager.Index(traitId);
TraitCategoryPrototype? categoryProto = null; TraitCategoryPrototype? categoryProto = null;
if (categoryId != null && categoryId != "default") if (categoryId != null && categoryId != "default")
categoryProto = prototypeManager.Index<TraitCategoryPrototype>(categoryId); categoryProto = prototypeManager.Index<TraitCategoryPrototype>(categoryId);
var list = new HashSet<string>(_traitPreferences); var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences);
if (pref) if (pref)
{ {
@@ -372,7 +413,7 @@ namespace Content.Shared.Preferences
var count = 0; var count = 0;
foreach (var trait in list) foreach (var trait in list)
{ {
var traitProtoTemp = prototypeManager.Index<TraitPrototype>(trait); var traitProtoTemp = prototypeManager.Index(trait);
count += traitProtoTemp.Cost; count += traitProtoTemp.Cost;
} }
@@ -514,7 +555,7 @@ namespace Content.Shared.Preferences
_ => SpawnPriorityPreference.None // Invalid enum values. _ => SpawnPriorityPreference.None // Invalid enum values.
}; };
var priorities = new Dictionary<string, JobPriority>(JobPriorities var priorities = new Dictionary<ProtoId<JobPrototype>, JobPriority>(JobPriorities
.Where(p => prototypeManager.TryIndex<JobPrototype>(p.Key, out var job) && job.SetPreference && p.Value switch .Where(p => prototypeManager.TryIndex<JobPrototype>(p.Key, out var job) && job.SetPreference && p.Value switch
{ {
JobPriority.Never => false, // Drop never since that's assumed default. JobPriority.Never => false, // Drop never since that's assumed default.
@@ -524,6 +565,17 @@ namespace Content.Shared.Preferences
_ => false _ => false
})); }));
var hasHighPrio = false;
foreach (var (key, value) in priorities)
{
if (value != JobPriority.High)
continue;
if (hasHighPrio)
priorities[key] = JobPriority.Medium;
hasHighPrio = true;
}
var antags = AntagPreferences var antags = AntagPreferences
.Where(id => prototypeManager.TryIndex<AntagPrototype>(id, out var antag) && antag.SetPreference) .Where(id => prototypeManager.TryIndex<AntagPrototype>(id, out var antag) && antag.SetPreference)
.ToList(); .ToList();

View File

@@ -63,8 +63,8 @@ namespace Content.Shared.Roles
public bool CanBeAntag { get; private set; } = true; public bool CanBeAntag { get; private set; } = true;
/// <summary> /// <summary>
/// Whether this job is a head. /// The "weight" or importance of this job. If this number is large, the job system will assign this job
/// The job system will try to pick heads before other jobs on the same priority level. /// before assigning other jobs.
/// </summary> /// </summary>
[DataField("weight")] [DataField("weight")]
public int Weight { get; private set; } public int Weight { get; private set; }

View File

@@ -118,6 +118,18 @@ public abstract class SharedJobSystem : EntitySystem
_prototypes.TryIndex(comp.Prototype, out prototype); _prototypes.TryIndex(comp.Prototype, out prototype);
} }
public bool MindTryGetJobId([NotNullWhen(true)] EntityUid? mindId, out ProtoId<JobPrototype>? job)
{
if (!TryComp(mindId, out JobComponent? comp))
{
job = null;
return false;
}
job = comp.Prototype;
return true;
}
/// <summary> /// <summary>
/// Tries to get the job name for this mind. /// Tries to get the job name for this mind.
/// Returns unknown if not found. /// Returns unknown if not found.

View File

@@ -10,7 +10,5 @@
- type: StationNameSetup - type: StationNameSetup
mapNameTemplate: "Meteor Arena" mapNameTemplate: "Meteor Arena"
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
Passenger: [ -1, -1 ] Passenger: [ -1, -1 ]

View File

@@ -14,8 +14,6 @@
!type:NanotrasenNameGenerator !type:NanotrasenNameGenerator
prefixCreator: 'R4' # R4407/Goon. GS isn't as cool sounding. prefixCreator: 'R4' # R4407/Goon. GS isn't as cool sounding.
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -16,8 +16,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_lox.yml emergencyShuttlePath: /Maps/Shuttles/emergency_lox.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -15,8 +15,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_box.yml emergencyShuttlePath: /Maps/Shuttles/emergency_box.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -16,8 +16,6 @@
!type:NanotrasenNameGenerator !type:NanotrasenNameGenerator
prefixCreator: '14' prefixCreator: '14'
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -18,8 +18,6 @@
- type: StationCargoShuttle - type: StationCargoShuttle
path: /Maps/Shuttles/cargo_core.yml path: /Maps/Shuttles/cargo_core.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Bartender: [ 2, 2 ] Bartender: [ 2, 2 ]

View File

@@ -10,8 +10,6 @@
- type: StationNameSetup - type: StationNameSetup
mapNameTemplate: "Empty" mapNameTemplate: "Empty"
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
Passenger: [ -1, -1 ] Passenger: [ -1, -1 ]
@@ -27,8 +25,6 @@
- type: StationNameSetup - type: StationNameSetup
mapNameTemplate: "Dev" mapNameTemplate: "Dev"
- type: StationJobs - type: StationJobs
overflowJobs:
- Captain
availableJobs: availableJobs:
Captain: [ -1, -1 ] Captain: [ -1, -1 ]
@@ -44,7 +40,5 @@
- type: StationNameSetup - type: StationNameSetup
mapNameTemplate: "TEG" mapNameTemplate: "TEG"
- type: StationJobs - type: StationJobs
overflowJobs:
- ChiefEngineer
availableJobs: availableJobs:
ChiefEngineer: [ -1, -1 ] ChiefEngineer: [ -1, -1 ]

View File

@@ -21,8 +21,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_transit.yml emergencyShuttlePath: /Maps/Shuttles/emergency_transit.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Bartender: [ 1, 1 ] Bartender: [ 1, 1 ]

View File

@@ -17,8 +17,6 @@
- type: StationCargoShuttle - type: StationCargoShuttle
path: /Maps/Shuttles/cargo_fland.yml path: /Maps/Shuttles/cargo_fland.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -16,8 +16,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_rod.yml emergencyShuttlePath: /Maps/Shuttles/emergency_rod.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -15,8 +15,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_meta.yml emergencyShuttlePath: /Maps/Shuttles/emergency_meta.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -15,8 +15,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_delta.yml emergencyShuttlePath: /Maps/Shuttles/emergency_delta.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -16,8 +16,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_omega.yml emergencyShuttlePath: /Maps/Shuttles/emergency_omega.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -15,8 +15,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_courser.yml emergencyShuttlePath: /Maps/Shuttles/emergency_courser.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -14,8 +14,6 @@
!type:NanotrasenNameGenerator !type:NanotrasenNameGenerator
prefixCreator: 'VG' prefixCreator: 'VG'
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -16,8 +16,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency.yml emergencyShuttlePath: /Maps/Shuttles/emergency.yml
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
Captain: [ 1, 1 ] Captain: [ 1, 1 ]
HeadOfSecurity: [ 1, 1 ] HeadOfSecurity: [ 1, 1 ]

View File

@@ -15,8 +15,6 @@
!type:NanotrasenNameGenerator !type:NanotrasenNameGenerator
prefixCreator: '14' prefixCreator: '14'
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]

View File

@@ -18,8 +18,6 @@
- type: StationEmergencyShuttle - type: StationEmergencyShuttle
emergencyShuttlePath: /Maps/Shuttles/emergency_omega.yml # To do - add railway station emergencyShuttlePath: /Maps/Shuttles/emergency_omega.yml # To do - add railway station
- type: StationJobs - type: StationJobs
overflowJobs:
- Passenger
availableJobs: availableJobs:
#service #service
Captain: [ 1, 1 ] Captain: [ 1, 1 ]