* Partial work on StationSystem refactor. * WIP station jobs API. * forgor to fire off grid events. * Partial implementation of StationSpawningSystem * whoops infinite loop. * Spawners should work now. * it compiles. * tfw * Vestigial code cleanup. * fix station deletion. * attempt to make tests go brr * add latejoin spawnpoints to test maps. * make sure the station still exists while destructing spawners. * forgot an exists check. * destruction order check. * hopefully fix final test. * fail-safe radstorm. * Deep-clean job code further. This is bugged!!!!! * Fix job bug. (init order moment) * whooo cleanup * New job selection algorithm that tries to distribute fairly across stations. * small nitpicks * Give the heads their weights to replace the head field. * make overflow assign take a station list. * moment * Fixes and test #1 of many. * please fix nullspace * AssignJobs should no longer even consider showing up on a trace. * add comment. * Introduce station configs, praying i didn't miss something. * in one small change stations are now fully serializable. * Further doc comments. * whoops. * Solve bug where assignjobs didn't account for roundstart. * Fix spawning, improve the API. Caught an oversight in stationsystem that should've broke everything but didn't, whoops. * Goodbye JobController. * minor fix.. * fix test fail, remove debug logs. * quick serialization fixes. * fixes.. * sus * partialing * Update Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs Co-authored-by: Kara <lunarautomaton6@gmail.com> * Use dirtying to avoid rebuilding the list 2,100 times. * add a bajillion more lines of docs (mostly in AssignJobs so i don't ever forget how it works) * Update Content.IntegrationTests/Tests/Station/StationJobsTest.cs Co-authored-by: Kara <lunarautomaton6@gmail.com> * Add the Mysteriously Missing Captain Check. * Put maprender back the way it belongs. * I love addressing reviews. * Update Content.Server/Station/Systems/StationJobsSystem.cs Co-authored-by: Kara <lunarautomaton6@gmail.com> * doc cleanup. * Fix bureaucratic error, add job slot tests. * zero cost abstractions when * cri * saner error. * Fix spawning failing certain tests due to gameticker not handling falliability correctly. Can't fix this until I refactor the rest of spawning code. * submodule gaming * Packedenger. * Documentation consistency. Co-authored-by: Kara <lunarautomaton6@gmail.com>
216 lines
8.9 KiB
C#
216 lines
8.9 KiB
C#
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<IPrototypeManager>();
|
|
var mapManager = server.ResolveDependency<IMapManager>();
|
|
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
|
|
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
|
|
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
|
|
var stationSystem = entSysMan.GetEntitySystem<StationSystem>();
|
|
|
|
List<EntityUid> 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<NetUserId, HumanoidCharacterProfile>()
|
|
.AddJob("TAssistant", JobPriority.Medium, PlayerCount)
|
|
.AddPreference("TClown", JobPriority.Low)
|
|
.AddPreference("TMime", JobPriority.High)
|
|
.WithPlayers(
|
|
new Dictionary<NetUserId, HumanoidCharacterProfile>()
|
|
.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<IPrototypeManager>();
|
|
var mapManager = server.ResolveDependency<IMapManager>();
|
|
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
|
|
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
|
|
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
|
|
var stationSystem = entSysMan.GetEntitySystem<StationSystem>();
|
|
|
|
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<NetUserId, HumanoidCharacterProfile> AddJob(
|
|
this Dictionary<NetUserId, HumanoidCharacterProfile> 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<NetUserId, HumanoidCharacterProfile> AddPreference(
|
|
this Dictionary<NetUserId, HumanoidCharacterProfile> inp, string jobId, JobPriority prio = JobPriority.Medium)
|
|
{
|
|
return inp.ToDictionary(x => x.Key, x => x.Value.WithJobPriority(jobId, prio));
|
|
}
|
|
|
|
public static Dictionary<NetUserId, HumanoidCharacterProfile> WithPlayers(
|
|
this Dictionary<NetUserId, HumanoidCharacterProfile> inp,
|
|
Dictionary<NetUserId, HumanoidCharacterProfile> second)
|
|
{
|
|
return new[] {inp, second}.SelectMany(x => x).ToDictionary(x => x.Key, x => x.Value);
|
|
}
|
|
}
|