]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add Job preference tests (#28625)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Wed, 5 Jun 2024 14:19:24 +0000 (02:19 +1200)
committerGitHub <noreply@github.com>
Wed, 5 Jun 2024 14:19:24 +0000 (00:19 +1000)
* Misc Job related changes

* Add JobTest

* A

* Aa

* Lets not confuse the yaml linter

* fixes

* a

44 files changed:
Content.Client/GameTicking/Managers/ClientGameTicker.cs
Content.Client/LateJoin/LateJoinGui.cs
Content.Client/Lobby/LobbyUIController.cs
Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs
Content.IntegrationTests/Pair/TestPair.Helpers.cs
Content.IntegrationTests/Pair/TestPair.Recycle.cs
Content.IntegrationTests/Pair/TestPair.cs
Content.IntegrationTests/PoolManager.Cvars.cs
Content.IntegrationTests/Tests/EntityTest.cs
Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
Content.IntegrationTests/Tests/PostMapInitTest.cs
Content.IntegrationTests/Tests/Round/JobTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Station/StationJobsTest.cs
Content.Server/Database/ServerDbBase.cs
Content.Server/GameTicking/GameTicker.RoundFlow.cs
Content.Server/Preferences/Managers/ServerPreferencesManager.cs
Content.Server/Spawners/Components/SpawnPointComponent.cs
Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs
Content.Server/Station/Components/StationJobsComponent.cs
Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs
Content.Server/Station/Systems/StationJobsSystem.cs
Content.Shared/GameTicking/SharedGameTicker.cs
Content.Shared/Preferences/HumanoidCharacterProfile.cs
Content.Shared/Roles/JobPrototype.cs
Content.Shared/Roles/Jobs/SharedJobSystem.cs
Resources/Prototypes/Maps/arenas.yml
Resources/Prototypes/Maps/atlas.yml
Resources/Prototypes/Maps/bagel.yml
Resources/Prototypes/Maps/box.yml
Resources/Prototypes/Maps/cluster.yml
Resources/Prototypes/Maps/core.yml
Resources/Prototypes/Maps/debug.yml
Resources/Prototypes/Maps/europa.yml
Resources/Prototypes/Maps/fland.yml
Resources/Prototypes/Maps/marathon.yml
Resources/Prototypes/Maps/meta.yml
Resources/Prototypes/Maps/oasis.yml
Resources/Prototypes/Maps/omega.yml
Resources/Prototypes/Maps/origin.yml
Resources/Prototypes/Maps/packed.yml
Resources/Prototypes/Maps/reach.yml
Resources/Prototypes/Maps/saltern.yml
Resources/Prototypes/Maps/train.yml

index 309db2eb4e6a30148b6927f673aba4cccc13b4a5..fcf5ae91a4971db3f829ded1c08df744650334c9 100644 (file)
@@ -4,10 +4,12 @@ using Content.Client.Lobby;
 using Content.Client.RoundEnd;
 using Content.Shared.GameTicking;
 using Content.Shared.GameWindow;
+using Content.Shared.Roles;
 using JetBrains.Annotations;
 using Robust.Client.Graphics;
 using Robust.Client.State;
 using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.GameTicking.Managers
 {
@@ -17,10 +19,9 @@ namespace Content.Client.GameTicking.Managers
         [Dependency] private readonly IStateManager _stateManager = default!;
         [Dependency] private readonly IClientAdminManager _admin = default!;
         [Dependency] private readonly IClyde _clyde = default!;
-        [Dependency] private readonly SharedMapSystem _map = 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();
 
         [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 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;
 
         public event Action? InfoBlobUpdated;
         public event Action? LobbyStatusUpdated;
         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()
         {
@@ -69,7 +70,7 @@ namespace Content.Client.GameTicking.Managers
             // 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)
 #if !DEBUG
-            _map.Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
+            EntityManager.System<SharedMapSystem>().Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
 #endif
         }
 
index 252aa9aafad48b46f24436e6c2b6dfc58a8b5638..62a06629f29443f71434634aa342dd7911c33b3f 100644 (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)
             {
@@ -337,10 +337,10 @@ namespace Content.Client.LateJoin
         public Label JobLabel { get; }
         public string JobId { get; }
         public string JobLocalisedName { get; }
-        public uint? Amount { get; private set; }
+        public int? Amount { get; private set; }
         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;
             JobId = jobId;
@@ -350,7 +350,7 @@ namespace Content.Client.LateJoin
             _initialised = true;
         }
 
-        public void RefreshLabel(uint? amount)
+        public void RefreshLabel(int? amount)
         {
             if (Amount == amount && _initialised)
             {
index f6a3eed962c1dbcbaed46b82dd77b371f681cd5e..05b98606ab9924080ab89caebfa6c2de04ac1ecb 100644 (file)
@@ -302,7 +302,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
     {
         var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
         // 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)
index 2ad8de7445f4d583da384843ecd74626151c180b..7efd1c594fb5caeb3e1709a861aeb475027a9574 100644 (file)
@@ -53,9 +53,9 @@ public sealed partial class CharacterPickerButton : ContainerButton
                 .LoadProfileEntity(humanoid, null, true);
 
             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}";
             }
         }
index 1e8306a02c606331b1029992b0af5f9c291f8605..588cf0d80e0435e74f75444e37bf2829bb546f28 100644 (file)
@@ -8,7 +8,6 @@ using Content.Shared.Roles;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
 using Robust.Shared.Network;
-using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.UnitTesting;
 
@@ -135,32 +134,73 @@ public sealed partial class TestPair
     }
 
     /// <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>
-    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)
     {
-        var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
+        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 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.SelectedCharacter;
+        var profile = (HumanoidCharacterProfile) prefs.Characters[0];
+        var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
 
-        Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value));
-        var newProfile = profile.WithAntagPreference(id, value);
+        // Automatic preference resetting only resets slot 0.
+        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;
+            }
+        }
+
+        foreach (var (job, priority) in priorities)
+        {
+            if (priority == JobPriority.Never)
+                dictionary.Remove(job);
+            else
+                dictionary[job] = priority;
+        }
 
-        // And why the fuck does it always create a new preference and profile object instead of just reusing them?
-        var newPrefs = prefMan.GetPreferences(user);
-        var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter;
-        Assert.That(newProf.AntagPreferences.Contains(id), Is.EqualTo(value));
+        var newProfile = profile.WithJobPriorities(dictionary);
+        _modifiedProfiles.Add(user);
+        await Server.WaitPost(() => prefMan.SetProfile(user, 0, newProfile).Wait());
     }
 }
index 4cae4affc4d24b55a5c4ff3e6af0000600b8f535..89a9eb64638c3ffd20ee84944e5f77324f650079 100644 (file)
@@ -2,10 +2,12 @@
 using System.IO;
 using System.Linq;
 using Content.Server.GameTicking;
+using Content.Server.Preferences.Managers;
 using Content.Shared.CCVar;
 using Content.Shared.GameTicking;
 using Content.Shared.Mind;
 using Content.Shared.Mind.Components;
+using Content.Shared.Preferences;
 using Robust.Client;
 using Robust.Server.Player;
 using Robust.Shared.Exceptions;
@@ -34,6 +36,9 @@ public sealed partial class TestPair : IAsyncDisposable
 
     private async Task OnCleanDispose()
     {
+        await Server.WaitIdleAsync();
+        await Client.WaitIdleAsync();
+        await ResetModifiedPreferences();
         await Server.RemoveAllDummySessions();
 
         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");
     }
 
+    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()
     {
         if (State != PairState.InUse)
index 0b18c3823903c5cb2a3bcfbe5ad9743a42172d94..0b681dcde10d477af8070007a5493f64daa99f6c 100644 (file)
@@ -26,6 +26,8 @@ public sealed partial class TestPair
     public readonly List<string> TestHistory = new();
     public PoolSettings Settings = default!;
     public TestMapData? TestMap;
+    private List<NetUserId> _modifiedProfiles = new();
+
     public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
     public RobustIntegrationTest.ClientIntegrationInstance Client { get;  private set; } = default!;
 
@@ -37,9 +39,7 @@ public sealed partial class TestPair
         client = Client;
     }
 
-    public ICommonSession? Player => Client.User == null
-        ? null
-        : Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User.Value);
+    public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
 
     public ContentPlayerData? PlayerData => Player?.Data.ContentData();
 
index 5acd9d502c1b122494a824206828b7d4d6564bf4..bcd48f82380579c1426a49654550a80aa85becf9 100644 (file)
@@ -28,6 +28,7 @@ public static partial class PoolManager
         (CCVars.EmergencyShuttleEnabled.Name, "false"),
         (CCVars.ProcgenPreload.Name,          "false"),
         (CCVars.WorldgenEnabled.Name,         "false"),
+        (CCVars.GatewayGeneratorEnabled.Name, "false"),
         (CVars.ReplayClientRecordingEnabled.Name, "false"),
         (CVars.ReplayServerRecordingEnabled.Name, "false"),
         (CCVars.GameDummyTicker.Name, "true"),
index 926374cf0500d568ea6f3be8bcb2e0efc0b2db01..1fc739fb0c70bca12cdac8bbbc55ef12f2018121 100644 (file)
@@ -340,6 +340,7 @@ namespace Content.IntegrationTests.Tests
                 "MapGrid",
                 "Broadphase",
                 "StationData", // errors when removed mid-round
+                "StationJobs",
                 "Actor", // We aren't testing actor components, those need their player session set.
                 "BlobFloorPlanBuilder", // Implodes if unconfigured.
                 "DebrisFeaturePlacerController", // Above.
index 662ea3b97470fa2ecc312a5f0f066a167809e67d..1bea33a82bcd504b0ff1b6c337616957ad59bd8f 100644 (file)
@@ -52,7 +52,7 @@ public sealed class AntagPreferenceTest
         Assert.That(pool.Count, Is.EqualTo(0));
 
         // 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.IsEntityValid(client.AttachedEntity, def), Is.True);
@@ -63,7 +63,7 @@ public sealed class AntagPreferenceTest
         Assert.That(sessions.Count, Is.EqualTo(1));
 
         // 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.IsEntityValid(client.AttachedEntity, def), Is.True);
index f56baba3426d2bbe23ca7ca1bfc848505afc0645..fc50d0bd334f141779f9763bc0f500c01a6026b4 100644 (file)
@@ -62,8 +62,8 @@ public sealed class NukeOpsTest
         await pair.RunTicksSync(5);
 
         // Opt into the nukies role.
-        await pair.SetAntagPref("NukeopsCommander", true);
-        await pair.SetAntagPref(dummies[1].UserId, "NukeopsMedic", true);
+        await pair.SetAntagPreference("NukeopsCommander", true);
+        await pair.SetAntagPreference( "NukeopsMedic", true, dummies[1].UserId);
 
         // Initially, the players have no attached entities
         Assert.That(pair.Player?.AttachedEntity, Is.Null);
@@ -236,8 +236,6 @@ public sealed class NukeOpsTest
         }
 
         ticker.SetGamePreset((GamePresetPrototype?)null);
-        await pair.SetAntagPref("NukeopsCommander", false);
-        await pair.SetAntagPref(dummies[1].UserId, "NukeopsMedic", false);
         await pair.CleanReturnAsync();
     }
 }
index 17c5e199a751d66322eedf74ffff4c17d5988259..2ae43898418155631c90033850731474a482fa99 100644 (file)
@@ -245,22 +245,15 @@ namespace Content.IntegrationTests.Tests
 
                     // Test all availableJobs have spawnPoints
                     // This is done inside gamemap test because loading the map takes ages and we already have it.
-                    var jobList = entManager.GetComponent<StationJobsComponent>(station).RoundStartJobList
-                        .Where(x => x.Value != 0)
-                        .Select(x => x.Key);
+                    var comp = entManager.GetComponent<StationJobsComponent>(station);
+                    var jobs = new HashSet<ProtoId<JobPrototype>>(comp.SetupAvailableJobs.Keys);
+
                     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);
-                    }
+                        .Where(x => x.SpawnType == SpawnPointType.Job)
+                        .Select(x => x.Job!.Value);
 
-                    Assert.That(missingSpawnPoints, Has.Count.EqualTo(0),
-                        $"There is no spawnpoint for {string.Join(", ", missingSpawnPoints)} on {mapProto}.");
+                    jobs.ExceptWith(spawnPoints);
+                    Assert.That(jobs, Is.Empty,$"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
                 }
 
                 try
diff --git a/Content.IntegrationTests/Tests/Round/JobTest.cs b/Content.IntegrationTests/Tests/Round/JobTest.cs
new file mode 100644 (file)
index 0000000..716e3cf
--- /dev/null
@@ -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();
+    }
+}
index 0085472c33cc31e5909c1ab4b027150529efa734..d68fdafb769e51e3173b30544a636d07dd8ed975 100644 (file)
@@ -7,7 +7,6 @@ using Content.Shared.Preferences;
 using Content.Shared.Roles;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Log;
-using Robust.Shared.Map;
 using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
@@ -46,8 +45,6 @@ public sealed class StationJobsTest
       stationProto: StandardNanotrasenStation
       components:
         - type: StationJobs
-          overflowJobs:
-          - Passenger
           availableJobs:
             TMime: [0, -1]
             TAssistant: [-1, -1]
@@ -164,7 +161,6 @@ public sealed class StationJobsTest
         var server = pair.Server;
 
         var prototypeManager = server.ResolveDependency<IPrototypeManager>();
-        var mapManager = server.ResolveDependency<IMapManager>();
         var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
         var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
         var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
@@ -215,6 +211,8 @@ public sealed class StationJobsTest
         var server = pair.Server;
 
         var prototypeManager = server.ResolveDependency<IPrototypeManager>();
+        var compFact = server.ResolveDependency<IComponentFactory>();
+        var name = compFact.GetComponentName<StationJobsComponent>();
 
         await server.WaitAssertion(() =>
         {
@@ -233,11 +231,14 @@ public sealed class StationJobsTest
                 {
                     foreach (var (stationId, station) in gameMap.Stations)
                     {
-                        if (!station.StationComponentOverrides.TryGetComponent("StationJobs", out var comp))
+                        if (!station.StationComponentOverrides.TryGetComponent(name, out var comp))
                             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.");
                         }
                     }
index be6c7196d5f9efde3a8d63efd681f458970be609..cd03af7087b9950c179f2ecfc88bd1db4ae2e615 100644 (file)
@@ -15,6 +15,7 @@ using Content.Shared.Humanoid.Markings;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Roles;
+using Content.Shared.Traits;
 using Microsoft.EntityFrameworkCore;
 using Robust.Shared.Enums;
 using Robust.Shared.Network;
@@ -183,9 +184,9 @@ namespace Content.Server.Database
 
         private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
         {
-            var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority);
-            var antags = profile.Antags.Select(a => a.AntagName);
-            var traits = profile.Traits.Select(t => t.TraitName);
+            var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
+            var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
+            var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
 
             var sex = Sex.Male;
             if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
index 6eb42b65c03a4933ba18ec5c2ae0334c8f6392bd..98fcc6441045da734b30ccb63f3d4ff26ad04d34 100644 (file)
@@ -239,7 +239,7 @@ namespace Content.Server.GameTicking
                 HumanoidCharacterProfile profile;
                 if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))
                 {
-                    profile = (HumanoidCharacterProfile) preferences.GetProfile(preferences.SelectedCharacterIndex);
+                    profile = (HumanoidCharacterProfile) preferences.SelectedCharacter;
                 }
                 else
                 {
index f7c15a234058abe6f06d5ebaf6244f25a362f7ff..8de458b6ee64ea12e8802ece00099ce8fcb67163 100644 (file)
@@ -305,11 +305,7 @@ namespace Content.Server.Preferences.Managers
             return usernames
                 .Select(p => (_cachedPlayerPrefs[p].Prefs, p))
                 .Where(p => p.Prefs != null)
-                .Select(p =>
-                {
-                    var idx = p.Prefs!.SelectedCharacterIndex;
-                    return new KeyValuePair<NetUserId, ICharacterProfile>(p.p, p.Prefs!.GetProfile(idx));
-                });
+                .Select(p => new KeyValuePair<NetUserId, ICharacterProfile>(p.p, p.Prefs!.SelectedCharacter));
         }
 
         internal static bool ShouldStorePrefs(LoginType loginType)
index 5cf231f224e828e0177bb874f7014b73167624fe..c6d14dfeb31c3206c132ae711bc0b1e38af7b6c1 100644 (file)
@@ -6,11 +6,8 @@ namespace Content.Server.Spawners.Components;
 [RegisterComponent]
 public sealed partial class SpawnPointComponent : Component, ISpawnPoint
 {
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField("job_id")]
-    private string? _jobId;
+    public ProtoId<JobPrototype>? Job;
 
     /// <summary>
     /// The type of spawn point
@@ -18,11 +15,9 @@ public sealed partial class SpawnPointComponent : Component, ISpawnPoint
     [DataField("spawn_type"), ViewVariables(VVAccess.ReadWrite)]
     public SpawnPointType SpawnType { get; set; } = SpawnPointType.Unset;
 
-    public JobPrototype? Job => string.IsNullOrEmpty(_jobId) ? null : _prototypeManager.Index<JobPrototype>(_jobId);
-
     public override string ToString()
     {
-        return $"{_jobId} {SpawnType}";
+        return $"{Job} {SpawnType}";
     }
 }
 
index 5be4ff10f4f40cd547b6dc4792f8020703e7fdca..be555dd54aca4dcc941bf67d9c48da3ba2604434 100644 (file)
@@ -39,7 +39,7 @@ public sealed class SpawnPointSystem : EntitySystem
 
             if (_gameTicker.RunLevel != GameRunLevel.InRound &&
                 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);
             }
index 74399bf412db442a2e887b0f97b8a8349b3795b0..3681ec9674f5ae146b2a60a04a55ab6cb393eae4 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.Station.Systems;
+using System.Linq;
+using Content.Server.Station.Systems;
 using Content.Shared.Roles;
 using JetBrains.Annotations;
 using Robust.Shared.Network;
@@ -14,25 +15,21 @@ namespace Content.Server.Station.Components;
 [RegisterComponent, Access(typeof(StationJobsSystem)), PublicAPI]
 public sealed partial class StationJobsComponent : Component
 {
-    /// <summary>
-    /// Total *round-start* jobs at station start.
-    /// </summary>
-    [DataField("roundStartTotalJobs")] public int RoundStartTotalJobs;
-
     /// <summary>
     /// Total *mid-round* jobs at station start.
+    /// This is inferred automatically from <see cref="SetupAvailableJobs"/>.
     /// </summary>
-    [DataField("midRoundTotalJobs")] public int MidRoundTotalJobs;
+    [ViewVariables] public int MidRoundTotalJobs;
 
     /// <summary>
     /// Current total jobs.
     /// </summary>
-    [DataField("totalJobs")] public int TotalJobs;
+    [DataField] public int TotalJobs;
 
     /// <summary>
     /// Station is running on extended access.
     /// </summary>
-    [DataField("extendedAccess")] public bool ExtendedAccess;
+    [DataField] public bool ExtendedAccess;
 
     /// <summary>
     /// 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>
     /// Set to -1 to disable extended access.
     /// </remarks>
-    [DataField("extendedAccessThreshold")]
+    [DataField]
     public int ExtendedAccessThreshold { get; set; } = 15;
 
     /// <summary>
@@ -54,28 +51,20 @@ public sealed partial class StationJobsComponent : Component
     public float? PercentJobsRemaining => MidRoundTotalJobs > 0 ? TotalJobs / (float) MidRoundTotalJobs : null;
 
     /// <summary>
-    /// The current list of jobs.
+    /// The current list of jobs of available jobs. Null implies that is no limit.
     /// </summary>
     /// <remarks>
     /// This should not be mutated or used directly unless you really know what you're doing, go through StationJobsSystem.
     /// </remarks>
-    [DataField("jobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<uint?, JobPrototype>))]
-    public Dictionary<string, uint?> 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();
+    [DataField]
+    public Dictionary<ProtoId<JobPrototype>, int?> JobList = new();
 
     /// <summary>
     /// Overflow jobs that round-start can spawn infinitely many of.
+    /// This is inferred automatically from <see cref="SetupAvailableJobs"/>.
     /// </summary>
-    [DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
-    public HashSet<string> OverflowJobs = new();
+    [ViewVariables]
+    public IReadOnlySet<ProtoId<JobPrototype>> OverflowJobs = default!;
 
     /// <summary>
     /// A dictionary relating a NetUserId to the jobs they have on station.
@@ -84,7 +73,10 @@ public sealed partial class StationJobsComponent : Component
     [DataField]
     public Dictionary<NetUserId, List<ProtoId<JobPrototype>>> PlayerJobs = new();
 
-    [DataField("availableJobs", required: true,
-        customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))]
-    public Dictionary<string, List<int?>> SetupAvailableJobs = default!;
+    /// <summary>
+    /// Mapping of jobs to an int[2] array that specifies jobs available at round start, and midround.
+    /// Negative values implies that there is no limit.
+    /// </summary>
+    [DataField("availableJobs", required: true)]
+    public Dictionary<ProtoId<JobPrototype>, int[]> SetupAvailableJobs = default!;
 }
index c3c3865c7bfaac996f3d8253bd2f8c4c883afb4c..e145e233e9e03b98ebea6775b720a6e5166bc9dd 100644 (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.
     /// A warning to all who enter ye cursed lands: This function is long and mildly incomprehensible. Best used without touching.
     /// </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);
 
         InitializeRoundStart();
 
         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.
         profiles = profiles.ShallowClone();
 
         // 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.
-        var stationJobs = new Dictionary<EntityUid, Dictionary<string, uint?>>();
+        var stationJobs = new Dictionary<EntityUid, Dictionary<ProtoId<JobPrototype>, int?>>();
         foreach (var station in stations)
         {
             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.
-        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)
         {
-            currentlySelectingJobs.Add(station, new Dictionary<string, uint?>());
+            currentlySelectingJobs.Add(station, new Dictionary<ProtoId<JobPrototype>, int?>());
         }
 
         // And these.
         // 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.
         var stationTotalSlots = new Dictionary<EntityUid, int>(stations.Count);
         // 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;
 
                 // 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.
                     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="profiles">Player character profiles.</param>
     /// <param name="stations">The stations to consider for spawn location.</param>
-    public void AssignOverflowJobs(ref Dictionary<NetUserId, (string?, EntityUid)> assignedJobs,
-        IEnumerable<NetUserId> allPlayersToAssign, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles, IReadOnlyList<EntityUid> stations)
+    public void AssignOverflowJobs(
+        ref Dictionary<NetUserId, (ProtoId<JobPrototype>?, EntityUid)> assignedJobs,
+        IEnumerable<NetUserId> allPlayersToAssign,
+        IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles,
+        IReadOnlyList<EntityUid> stations)
     {
         var givenStations = stations.ToList();
         if (givenStations.Count == 0)
index 3bfa815af1ef6a277166537bcc6d932d09cb4304..307610d136fe3e6eee6ecc5f51984747fc8db017 100644 (file)
@@ -3,6 +3,7 @@ using System.Linq;
 using Content.Server.GameTicking;
 using Content.Server.Station.Components;
 using Content.Shared.CCVar;
+using Content.Shared.FixedPoint;
 using Content.Shared.GameTicking;
 using Content.Shared.Preferences;
 using Content.Shared.Roles;
@@ -31,12 +32,25 @@ public sealed partial class StationJobsSystem : EntitySystem
     public override void Initialize()
     {
         SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialized);
+        SubscribeLocalEvent<StationJobsComponent, ComponentInit>(OnInit);
         SubscribeLocalEvent<StationJobsComponent, StationRenamedEvent>(OnStationRenamed);
         SubscribeLocalEvent<StationJobsComponent, ComponentShutdown>(OnStationDeletion);
         SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
         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 _)
     {
         if (_availableJobsDirty)
@@ -57,28 +71,11 @@ public sealed partial class StationJobsSystem : EntitySystem
         if (!TryComp<StationJobsComponent>(msg.Station, out var stationJobs))
             return;
 
-        var mapJobList = stationJobs.SetupAvailableJobs;
-
-        stationJobs.RoundStartTotalJobs = mapJobList.Values.Where(x => x[0] is not null && x[0] > 0).Sum(x => x[0]!.Value);
-        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.JobList = stationJobs.SetupAvailableJobs.ToDictionary(
+            x => x.Key,
+            x=> (int?)(x.Value[1] < 0 ? null : x.Value[1]));
 
-        stationJobs.OverflowJobs = stationJobs.OverflowJobs.ToHashSet();
+        stationJobs.TotalJobs = stationJobs.JobList.Values.Select(x => x ?? 0).Sum();
 
         UpdateJobsAvailable();
     }
@@ -141,7 +138,11 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
     /// <returns>Whether or not slot adjustment was a success.</returns>
     /// <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)
     {
         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 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.
-        switch (jobList.ContainsKey(jobPrototypeId))
+
+        if (amount == 0)
+            return true;
+
+        switch (jobList.TryGetValue(jobPrototypeId, out var available))
         {
             case false when amount < 0:
                 return false;
@@ -164,31 +169,20 @@ public sealed partial class StationJobsSystem : EntitySystem
                 if (!createSlot)
                     return false;
                 stationJobs.TotalJobs += amount;
-                jobList[jobPrototypeId] = (uint?)amount;
+                jobList[jobPrototypeId] = amount;
                 UpdateJobsAvailable();
                 return true;
             case true:
                 // Job is unlimited so just say we adjusted it and do nothing.
-                if (jobList[jobPrototypeId] == null)
+                if (available is not {} avail)
                     return true;
 
                 // Would remove more jobs than we have available.
-                if (amount < 0 && (jobList[jobPrototypeId] + amount < 0 && !clamp))
+                if (available + amount < 0 && !clamp)
                     return false;
 
-                stationJobs.TotalJobs += amount;
-
-                //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);
-                }
-
+                jobList[jobPrototypeId] = Math.Max(avail + amount, 0);
+                stationJobs.TotalJobs = jobList.Values.Select(x => x ?? 0).Sum();
                 UpdateJobsAvailable();
                 return true;
         }
@@ -239,7 +233,10 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
     /// <returns>Whether or not setting the value succeeded.</returns>
     /// <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)
     {
         if (!Resolve(station, ref stationJobs))
@@ -255,13 +252,13 @@ public sealed partial class StationJobsSystem : EntitySystem
                 if (!createSlot)
                     return false;
                 stationJobs.TotalJobs += amount;
-                jobList[jobPrototypeId] = (uint?)amount;
+                jobList[jobPrototypeId] = amount;
                 UpdateJobsAvailable();
                 return true;
             case true:
-                stationJobs.TotalJobs += amount - (int) (jobList[jobPrototypeId] ?? 0);
+                stationJobs.TotalJobs += amount - (jobList[jobPrototypeId] ?? 0);
 
-                jobList[jobPrototypeId] = (uint)amount;
+                jobList[jobPrototypeId] = amount;
                 UpdateJobsAvailable();
                 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));
 
         // Subtract out the job we're fixing to make have unlimited slots.
-        if (stationJobs.JobList.ContainsKey(jobPrototypeId) && stationJobs.JobList[jobPrototypeId] != null)
-            stationJobs.TotalJobs -= (int)stationJobs.JobList[jobPrototypeId]!.Value;
+        if (stationJobs.JobList.TryGetValue(jobPrototypeId, out var existing))
+            stationJobs.TotalJobs -= existing ?? 0;
 
         stationJobs.JobList[jobPrototypeId] = null;
 
@@ -319,8 +316,7 @@ public sealed partial class StationJobsSystem : EntitySystem
         if (!Resolve(station, ref stationJobs))
             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 res;
+        return stationJobs.JobList.TryGetValue(jobPrototypeId, out var job) && job == null;
     }
 
     /// <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="slots">The number of slots remaining. Null if infinite.</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);
     }
@@ -343,21 +339,12 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// <returns>Whether or not the slot exists.</returns>
     /// <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>
-    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))
             throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
 
-        if (stationJobs.JobList.TryGetValue(jobPrototypeId, out var job))
-        {
-            slots = job;
-            return true;
-        }
-        else // Else if slot isn't present return null.
-        {
-            slots = null;
-            return false;
-        }
+        return stationJobs.JobList.TryGetValue(jobPrototypeId, out slots);
     }
 
     /// <summary>
@@ -367,12 +354,14 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
     /// <returns>Set containing all jobs available.</returns>
     /// <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))
             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>
@@ -382,12 +371,12 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
     /// <returns>Set containing all overflow jobs available.</returns>
     /// <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))
             throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
 
-        return stationJobs.OverflowJobs.ToHashSet();
+        return stationJobs.OverflowJobs;
     }
 
     /// <summary>
@@ -397,7 +386,7 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
     /// <returns>List of all jobs on the station.</returns>
     /// <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))
             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>
     /// <returns>List of all round-start jobs.</returns>
     /// <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))
             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>
@@ -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="disallowedJobs">A set of disallowed jobs, if any.</param>
     /// <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)
             return null;
 
         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
                 .Where(p =>
@@ -474,7 +465,10 @@ public sealed partial class StationJobsSystem : EntitySystem
             return null;
 
         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
@@ -483,7 +477,7 @@ public sealed partial class StationJobsSystem : EntitySystem
 
     private bool _availableJobsDirty;
 
-    private TickerJobsAvailableEvent _cachedAvailableJobs = new (new Dictionary<NetEntity, string>(), new Dictionary<NetEntity, Dictionary<string, uint?>>());
+    private TickerJobsAvailableEvent _cachedAvailableJobs = new(new(), new());
 
     /// <summary>
     /// 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 (_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 query = EntityQueryEnumerator<StationJobsComponent>();
index 95da4f4c38d33a3cbf7c8e3220866a1f3e810b90..308476baa8d6d283cb1f58bb402a91d035733740 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Roles;
 using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Replays;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.Markdown.Mapping;
@@ -128,19 +129,17 @@ namespace Content.Shared.GameTicking
     }
 
     [Serializable, NetSerializable]
-    public sealed class TickerJobsAvailableEvent : EntityEventArgs
+    public sealed class TickerJobsAvailableEvent(
+        Dictionary<NetEntity, string> stationNames,
+        Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> jobsAvailableByStation)
+        : EntityEventArgs
     {
         /// <summary>
         /// The Status of the Player in the lobby (ready, observer, ...)
         /// </summary>
-        public Dictionary<NetEntity, Dictionary<string, uint?>> JobsAvailableByStation { get; }
-        public Dictionary<NetEntity, string> StationNames { get; }
+        public Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailableByStation { get; } = jobsAvailableByStation;
 
-        public TickerJobsAvailableEvent(Dictionary<NetEntity, string> stationNames, Dictionary<NetEntity, Dictionary<string, uint?>> jobsAvailableByStation)
-        {
-            StationNames = stationNames;
-            JobsAvailableByStation = jobsAvailableByStation;
-        }
+        public Dictionary<NetEntity, string> StationNames { get; } = stationNames;
     }
 
     [Serializable, NetSerializable, DataDefinition]
index bd55bbb40a8ffce82a5cf879757fbc3aff16d0f8..20c54cd2687bbed15d0a87c15dd80c76e0d60f53 100644 (file)
@@ -35,7 +35,7 @@ namespace Content.Shared.Preferences
         /// Job preferences for initial spawn.
         /// </summary>
         [DataField]
-        private Dictionary<string, JobPriority> _jobPriorities = new()
+        private Dictionary<ProtoId<JobPrototype>, JobPriority> _jobPriorities = new()
         {
             {
                 SharedGameTicker.FallbackOverflowJob, JobPriority.High
@@ -46,13 +46,13 @@ namespace Content.Shared.Preferences
         /// Antags we have opted in to.
         /// </summary>
         [DataField]
-        private HashSet<string> _antagPreferences = new();
+        private HashSet<ProtoId<AntagPrototype>> _antagPreferences = new();
 
         /// <summary>
         /// Enabled traits.
         /// </summary>
         [DataField]
-        private HashSet<string> _traitPreferences = new();
+        private HashSet<ProtoId<TraitPrototype>> _traitPreferences = new();
 
         /// <summary>
         /// <see cref="_loadouts"/>
@@ -75,7 +75,7 @@ namespace Content.Shared.Preferences
         /// Associated <see cref="SpeciesPrototype"/> for this profile.
         /// </summary>
         [DataField]
-        public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
+        public ProtoId<SpeciesPrototype> Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
 
         [DataField]
         public int Age { get; set; } = 18;
@@ -106,17 +106,17 @@ namespace Content.Shared.Preferences
         /// <summary>
         /// <see cref="_jobPriorities"/>
         /// </summary>
-        public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
+        public IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> JobPriorities => _jobPriorities;
 
         /// <summary>
         /// <see cref="_antagPreferences"/>
         /// </summary>
-        public IReadOnlySet<string> AntagPreferences => _antagPreferences;
+        public IReadOnlySet<ProtoId<AntagPrototype>> AntagPreferences => _antagPreferences;
 
         /// <summary>
         /// <see cref="_traitPreferences"/>
         /// </summary>
-        public IReadOnlySet<string> TraitPreferences => _traitPreferences;
+        public IReadOnlySet<ProtoId<TraitPrototype>> TraitPreferences => _traitPreferences;
 
         /// <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.
@@ -134,10 +134,10 @@ namespace Content.Shared.Preferences
             Gender gender,
             HumanoidCharacterAppearance appearance,
             SpawnPriorityPreference spawnPriority,
-            Dictionary<string, JobPriority> jobPriorities,
+            Dictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities,
             PreferenceUnavailableMode preferenceUnavailable,
-            HashSet<string> antagPreferences,
-            HashSet<string> traitPreferences,
+            HashSet<ProtoId<AntagPrototype>> antagPreferences,
+            HashSet<ProtoId<TraitPrototype>> traitPreferences,
             Dictionary<string, RoleLoadout> loadouts)
         {
             Name = name;
@@ -153,6 +153,20 @@ namespace Content.Shared.Preferences
             _antagPreferences = antagPreferences;
             _traitPreferences = traitPreferences;
             _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>
@@ -165,10 +179,10 @@ namespace Content.Shared.Preferences
                 other.Gender,
                 other.Appearance.Clone(),
                 other.SpawnPriority,
-                new Dictionary<string, JobPriority>(other.JobPriorities),
+                new Dictionary<ProtoId<JobPrototype>, JobPriority>(other.JobPriorities),
                 other.PreferenceUnavailable,
-                new HashSet<string>(other.AntagPreferences),
-                new HashSet<string>(other.TraitPreferences),
+                new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
+                new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
                 new Dictionary<string, RoleLoadout>(other.Loadouts))
         {
         }
@@ -289,21 +303,48 @@ namespace Content.Shared.Preferences
             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)
             {
-                _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)
             {
                 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
             {
                 dictionary[jobId] = priority;
@@ -320,17 +361,17 @@ namespace Content.Shared.Preferences
             return new(this) { PreferenceUnavailable = mode };
         }
 
-        public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
+        public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<ProtoId<AntagPrototype>> antagPreferences)
         {
             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)
             {
                 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 traitProto = prototypeManager.Index<TraitPrototype>(traitId);
+            var traitProto = prototypeManager.Index(traitId);
 
             TraitCategoryPrototype? categoryProto = null;
             if (categoryId != null && categoryId != "default")
                 categoryProto = prototypeManager.Index<TraitCategoryPrototype>(categoryId);
 
-            var list = new HashSet<string>(_traitPreferences);
+            var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences);
 
             if (pref)
             {
@@ -372,7 +413,7 @@ namespace Content.Shared.Preferences
                 var count = 0;
                 foreach (var trait in list)
                 {
-                    var traitProtoTemp = prototypeManager.Index<TraitPrototype>(trait);
+                    var traitProtoTemp = prototypeManager.Index(trait);
                     count += traitProtoTemp.Cost;
                 }
 
@@ -514,7 +555,7 @@ namespace Content.Shared.Preferences
                 _ => 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
                 {
                     JobPriority.Never => false, // Drop never since that's assumed default.
@@ -524,6 +565,17 @@ namespace Content.Shared.Preferences
                     _ => 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
                 .Where(id => prototypeManager.TryIndex<AntagPrototype>(id, out var antag) && antag.SetPreference)
                 .ToList();
index 0b3bfb44384cb8de1bd32e6a99a3f478cefe1ffa..dd67e7b10474f2a66017a5400daee6badc9739a6 100644 (file)
@@ -63,8 +63,8 @@ namespace Content.Shared.Roles
         public bool CanBeAntag { get; private set; } = true;
 
         /// <summary>
-        ///     Whether this job is a head.
-        ///     The job system will try to pick heads before other jobs on the same priority level.
+        ///     The "weight" or importance of this job. If this number is large, the job system will assign this job
+        ///     before assigning other jobs.
         /// </summary>
         [DataField("weight")]
         public int Weight { get; private set; }
index fcf76052785e04beb3249642031dcfa8612e5f36..ce4428d9fea7b766d8a1075d2e75284698528324 100644 (file)
@@ -118,6 +118,18 @@ public abstract class SharedJobSystem : EntitySystem
                _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>
     ///     Tries to get the job name for this mind.
     ///     Returns unknown if not found.
index 32f85437225a6d9fb5f1eddcd6be5ff8277cac65..7ad7a16bc2a914e02fec7a0576b21f51fde33453 100644 (file)
@@ -10,7 +10,5 @@
         - type: StationNameSetup
           mapNameTemplate: "Meteor Arena"
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             Passenger: [ -1, -1 ]
index ef7523c72795374af770face73016df846a11e8c..6fe3eff030ad5808495fe6f9c4047c6c3b0284ae 100644 (file)
@@ -14,8 +14,6 @@
             !type:NanotrasenNameGenerator
             prefixCreator: 'R4' # R4407/Goon. GS isn't as cool sounding.
         - type: StationJobs
-          overflowJobs:
-          - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 24ca17339fcc8f167a7db72c2bb6975177446d52..ea06153d7e45369b0d81ad847475ad1902868460 100644 (file)
@@ -16,8 +16,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_lox.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 50826fc5e097c4da1fe576340f8404f5d0381b7c..89ba3779c657ac52f0ea5cda11629e9ada297dc2 100644 (file)
@@ -15,8 +15,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_box.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index fe445b00813441260c8480c453f99532db7e71a5..10a12c4f44e2456222f50f693dc04fdd6c3645c2 100644 (file)
@@ -16,8 +16,6 @@
             !type:NanotrasenNameGenerator
             prefixCreator: '14'
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 6b85aca51d3862b9c73868cbe85bd7fb97aa67d8..d7a15f2b1da4a82990a7c81890d5a804086edc47 100644 (file)
@@ -18,8 +18,6 @@
         - type: StationCargoShuttle
           path: /Maps/Shuttles/cargo_core.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Bartender: [ 2, 2 ]
index 2f475c1e5795349c2b8eb6468de5a22448e4750e..8d4cc550a27c9723076e342d982ede0e36e3f9cb 100644 (file)
@@ -10,8 +10,6 @@
         - type: StationNameSetup
           mapNameTemplate: "Empty"
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             Passenger: [ -1, -1 ]
 
@@ -27,8 +25,6 @@
         - type: StationNameSetup
           mapNameTemplate: "Dev"
         - type: StationJobs
-          overflowJobs:
-          - Captain
           availableJobs:
             Captain: [ -1, -1 ]
 
@@ -44,7 +40,5 @@
         - type: StationNameSetup
           mapNameTemplate: "TEG"
         - type: StationJobs
-          overflowJobs:
-            - ChiefEngineer
           availableJobs:
             ChiefEngineer: [ -1, -1 ]
index 0c9f1d975b25f73f1a5c19630f62cdc8af2f2d7e..412e1b46569fad17a5171376e8dbefab2ee38ff4 100644 (file)
@@ -21,8 +21,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_transit.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Bartender: [ 1, 1 ]
index f0c35f99e57ef77c16c6f9714f58b3703d236ce2..f22c44cb630064d1b38586d28347de7c7df8a3bc 100644 (file)
@@ -17,8 +17,6 @@
         - type: StationCargoShuttle
           path: /Maps/Shuttles/cargo_fland.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index f82ee1d4344e97ab10a1aff27ad3979b7bce15bb..32ad8d576c24c7089ce1bd6f8a8e34798765ae70 100644 (file)
@@ -16,8 +16,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_rod.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 2bee606e95abf51e20a59bb3cd7734028095f8c3..ebd6954aa763f77d605682c93502cd092b345483 100644 (file)
@@ -15,8 +15,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_meta.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index b5e0f97190a821cc9d28f64ad11d7dacdc61cdb6..a4cc6eb43d625a4dcf80c2c54f1f511d8a68d693 100644 (file)
@@ -15,8 +15,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_delta.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
@@ -62,4 +60,4 @@
             Passenger: [ -1, -1 ]
             Clown: [ 1, 1 ]
             Mime: [ 1, 1 ]
-            Musician: [ 1, 1 ]
\ No newline at end of file
+            Musician: [ 1, 1 ]
index f90c5f5b658d69966c3eadd75743eaa54034b2a1..b94fdbc05d189f91ac848e21bd36e6a0c84573b3 100644 (file)
@@ -16,8 +16,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_omega.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 128148989129d19f873b3d1107095cb343fe165d..24214b37a1f3405c27f36c4ed8361223e82d816a 100644 (file)
@@ -15,8 +15,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_courser.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 20d6c7a7bd7bca2cb292929e2023f8887e32d347..b844636bf8315f71e2c5208e765dbd6541981a39 100644 (file)
@@ -14,8 +14,6 @@
             !type:NanotrasenNameGenerator
             prefixCreator: 'VG'
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index 03a688cf23c8a4fc6fe65fe160a09530c4c6d5ae..a0d6752c1fb8bf91214c9105276f0b780a7c507c 100644 (file)
@@ -16,8 +16,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency.yml
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             Captain: [ 1, 1 ]
             HeadOfSecurity: [ 1, 1 ]
index bc12eca65461db37cb42fe9048c848358a969644..e9d26ce3192ce2aedf767ddcd97c93fc490d2125 100644 (file)
@@ -15,8 +15,6 @@
             !type:NanotrasenNameGenerator
             prefixCreator: '14'
         - type: StationJobs
-          overflowJobs:
-          - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]
index b18db7aea8d12d58015a66f8f3b6d417935d0127..7f24fcdd677188d412a68f1dbeabc63d4dbd3ea6 100644 (file)
@@ -18,8 +18,6 @@
         - type: StationEmergencyShuttle
           emergencyShuttlePath: /Maps/Shuttles/emergency_omega.yml # To do - add railway station
         - type: StationJobs
-          overflowJobs:
-            - Passenger
           availableJobs:
             #service
             Captain: [ 1, 1 ]