using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Content.Server.Administration.Managers; using Content.Server.Maps; using Content.Server.Station.Systems; using Content.Shared.Preferences; using NUnit.Framework; using Robust.Server; using Robust.Shared.GameObjects; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests.Station; [TestFixture] [TestOf(typeof(StationJobsSystem))] public sealed class StationJobsTest : ContentIntegrationTest { private const string Prototypes = @" - type: gameMap id: FooStation minPlayers: 0 mapName: FooStation mapPath: Maps/Tests/empty.yml stations: Station: mapNameTemplate: FooStation overflowJobs: - Assistant availableJobs: TMime: [0, -1] TAssistant: [-1, -1] TCaptain: [5, 5] TClown: [5, 6] - type: job id: TAssistant - type: job id: TMime weight: 20 - type: job id: TClown weight: -10 - type: job id: TCaptain weight: 10 - type: job id: TChaplain "; private const int StationCount = 100; private const int CaptainCount = StationCount; private const int PlayerCount = 2000; private const int TotalPlayers = PlayerCount + CaptainCount; [Test] public async Task AssignJobsTest() { var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes, Options = new ServerOptions() { LoadContentResources = false }}; var server = StartServer(options); await server.WaitIdleAsync(); var prototypeManager = server.ResolveDependency(); var mapManager = server.ResolveDependency(); var fooStationProto = prototypeManager.Index("FooStation"); var entSysMan = server.ResolveDependency().EntitySysManager; var stationJobs = entSysMan.GetEntitySystem(); var stationSystem = entSysMan.GetEntitySystem(); List stations = new(); await server.WaitPost(() => { mapManager.CreateNewMapEntity(MapId.Nullspace); for (var i = 0; i < StationCount; i++) { stations.Add(stationSystem.InitializeNewStation(fooStationProto.Stations["Station"], null, $"Foo {StationCount}")); } }); await server.WaitAssertion(() => { var fakePlayers = new Dictionary() .AddJob("TAssistant", JobPriority.Medium, PlayerCount) .AddPreference("TClown", JobPriority.Low) .AddPreference("TMime", JobPriority.High) .WithPlayers( new Dictionary() .AddJob("TCaptain", JobPriority.High, CaptainCount) ); var start = new Stopwatch(); start.Start(); var assigned = stationJobs.AssignJobs(fakePlayers, stations); var time = start.Elapsed.TotalMilliseconds; Logger.Info($"Took {time} ms to distribute {TotalPlayers} players."); foreach (var station in stations) { var assignedHere = assigned .Where(x => x.Value.Item2 == station) .ToDictionary(x => x.Key, x => x.Value); // Each station should have SOME players. Assert.That(assignedHere, Is.Not.Empty); // And it should have at least the minimum players to be considered a "fair" share, as they're all the same. Assert.That(assignedHere, Has.Count.GreaterThanOrEqualTo(TotalPlayers/stations.Count), "Station has too few players."); // And it shouldn't have ALL the players, either. Assert.That(assignedHere, Has.Count.LessThan(TotalPlayers), "Station has too many players."); // And there should be *A* captain, as there's one player with captain enabled per station. Assert.That(assignedHere.Where(x => x.Value.Item1 == "TCaptain").ToList(), Has.Count.EqualTo(1)); } // All clown players have assistant as a higher priority. Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Not.Contain("TClown")); // Mime isn't an open job-slot at round-start. Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Not.Contain("TMime")); // All players have slots they can fill. Assert.That(assigned.Values, Has.Count.EqualTo(TotalPlayers), $"Expected {TotalPlayers} players."); // There must be assistants present. Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TAssistant")); // There must be captains present, too. Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TCaptain")); }); } [Test] public async Task AdjustJobsTest() { var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes, Options = new ServerOptions() { LoadContentResources = false }}; var server = StartServer(options); await server.WaitIdleAsync(); var prototypeManager = server.ResolveDependency(); var mapManager = server.ResolveDependency(); var fooStationProto = prototypeManager.Index("FooStation"); var entSysMan = server.ResolveDependency().EntitySysManager; var stationJobs = entSysMan.GetEntitySystem(); var stationSystem = entSysMan.GetEntitySystem(); var station = EntityUid.Invalid; await server.WaitPost(() => { mapManager.CreateNewMapEntity(MapId.Nullspace); station = stationSystem.InitializeNewStation(fooStationProto.Stations["Station"], null, $"Foo Station"); }); await server.WaitAssertion(() => { // Verify jobs are/are not unlimited. Assert.Multiple(() => { Assert.That(stationJobs.IsJobUnlimited(station, "TAssistant"), "TAssistant is expected to be unlimited."); Assert.That(stationJobs.IsJobUnlimited(station, "TMime"), "TMime is expected to be unlimited."); Assert.That(!stationJobs.IsJobUnlimited(station, "TCaptain"), "TCaptain is expected to not be unlimited."); Assert.That(!stationJobs.IsJobUnlimited(station, "TClown"), "TClown is expected to not be unlimited."); }); Assert.Multiple(() => { Assert.That(stationJobs.TrySetJobSlot(station, "TClown", 0), "Could not set TClown to have zero slots."); Assert.That(stationJobs.TryGetJobSlot(station, "TClown", out var clownSlots), "Could not get the number of TClown slots."); Assert.That(clownSlots, Is.EqualTo(0)); Assert.That(!stationJobs.TryAdjustJobSlot(station, "TCaptain", -9999), "Was able to adjust TCaptain by -9999 without clamping."); Assert.That(stationJobs.TryAdjustJobSlot(station, "TCaptain", -9999, false, true), "Could not adjust TCaptain by -9999."); Assert.That(stationJobs.TryGetJobSlot(station, "TCaptain", out var captainSlots), "Could not get the number of TCaptain slots."); Assert.That(captainSlots, Is.EqualTo(0)); }); Assert.Multiple(() => { Assert.That(stationJobs.TrySetJobSlot(station, "TChaplain", 10, true), "Could not create 10 TChaplain slots."); stationJobs.MakeJobUnlimited(station, "TChaplain"); Assert.That(stationJobs.IsJobUnlimited(station, "TChaplain"), "Could not make TChaplain unlimited."); }); }); } } internal static class JobExtensions { public static Dictionary AddJob( this Dictionary inp, string jobId, JobPriority prio = JobPriority.Medium, int amount = 1) { for (var i = 0; i < amount; i++) { inp.Add(new NetUserId(Guid.NewGuid()), HumanoidCharacterProfile.Random().WithJobPriority(jobId, prio)); } return inp; } public static Dictionary AddPreference( this Dictionary inp, string jobId, JobPriority prio = JobPriority.Medium) { return inp.ToDictionary(x => x.Key, x => x.Value.WithJobPriority(jobId, prio)); } public static Dictionary WithPlayers( this Dictionary inp, Dictionary second) { return new[] {inp, second}.SelectMany(x => x).ToDictionary(x => x.Key, x => x.Value); } }