Skip to content

Commit 3618133

Browse files
StationSystem/jobs/partial spawning refactor (#7580)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]>
1 parent d234a79 commit 3618133

65 files changed

Lines changed: 2564 additions & 1368 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Content.Client/GameTicking/Managers/ClientGameTicker.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using Content.Client.Viewport;
44
using Content.Shared.GameTicking;
55
using Content.Shared.GameWindow;
6-
using Content.Shared.Station;
76
using JetBrains.Annotations;
87
using Robust.Client.Graphics;
98
using Robust.Client.State;
@@ -17,8 +16,8 @@ public sealed class ClientGameTicker : SharedGameTicker
1716
{
1817
[Dependency] private readonly IStateManager _stateManager = default!;
1918
[ViewVariables] private bool _initialized;
20-
private Dictionary<StationId, Dictionary<string, int>> _jobsAvailable = new();
21-
private Dictionary<StationId, string> _stationNames = new();
19+
private Dictionary<EntityUid, Dictionary<string, uint?>> _jobsAvailable = new();
20+
private Dictionary<EntityUid, string> _stationNames = new();
2221

2322
[ViewVariables] public bool AreWeReady { get; private set; }
2423
[ViewVariables] public bool IsGameStarted { get; private set; }
@@ -29,14 +28,14 @@ public sealed class ClientGameTicker : SharedGameTicker
2928
[ViewVariables] public TimeSpan StartTime { get; private set; }
3029
[ViewVariables] public new bool Paused { get; private set; }
3130
[ViewVariables] public Dictionary<NetUserId, LobbyPlayerStatus> Status { get; private set; } = new();
32-
[ViewVariables] public IReadOnlyDictionary<StationId, Dictionary<string, int>> JobsAvailable => _jobsAvailable;
33-
[ViewVariables] public IReadOnlyDictionary<StationId, string> StationNames => _stationNames;
31+
[ViewVariables] public IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>> JobsAvailable => _jobsAvailable;
32+
[ViewVariables] public IReadOnlyDictionary<EntityUid, string> StationNames => _stationNames;
3433

3534
public event Action? InfoBlobUpdated;
3635
public event Action? LobbyStatusUpdated;
3736
public event Action? LobbyReadyUpdated;
3837
public event Action? LobbyLateJoinStatusUpdated;
39-
public event Action<IReadOnlyDictionary<StationId, Dictionary<string, int>>>? LobbyJobsAvailableUpdated;
38+
public event Action<IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>>>? LobbyJobsAvailableUpdated;
4039

4140
public override void Initialize()
4241
{

Content.Client/LateJoin/LateJoinGui.cs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
using System;
2-
using System.Collections.Generic;
31
using System.Linq;
42
using Content.Client.GameTicking.Managers;
53
using Content.Client.HUD.UI;
64
using Content.Shared.Roles;
7-
using Content.Shared.Station;
85
using Robust.Client.Console;
96
using Robust.Client.UserInterface;
107
using Robust.Client.UserInterface.Controls;
118
using Robust.Client.UserInterface.CustomControls;
129
using Robust.Client.Utility;
13-
using Robust.Shared.GameObjects;
14-
using Robust.Shared.IoC;
15-
using Robust.Shared.Localization;
16-
using Robust.Shared.Log;
17-
using Robust.Shared.Maths;
1810
using Robust.Shared.Prototypes;
1911
using Robust.Shared.Utility;
2012
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -26,10 +18,10 @@ public sealed class LateJoinGui : DefaultWindow
2618
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
2719
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
2820

29-
public event Action<(StationId, string)> SelectedId;
21+
public event Action<(EntityUid, string)> SelectedId;
3022

31-
private readonly Dictionary<StationId, Dictionary<string, JobButton>> _jobButtons = new();
32-
private readonly Dictionary<StationId, Dictionary<string, BoxContainer>> _jobCategories = new();
23+
private readonly Dictionary<EntityUid, Dictionary<string, JobButton>> _jobButtons = new();
24+
private readonly Dictionary<EntityUid, Dictionary<string, BoxContainer>> _jobCategories = new();
3325
private readonly List<ScrollContainer> _jobLists = new();
3426

3527
private readonly Control _base;
@@ -57,7 +49,7 @@ public LateJoinGui()
5749
{
5850
var (station, jobId) = x;
5951
Logger.InfoS("latejoin", $"Late joining as ID: {jobId}");
60-
_consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)} {station.Id}");
52+
_consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)} {station}");
6153
Close();
6254
};
6355

@@ -209,7 +201,7 @@ private void RebuildUI()
209201

210202
var jobLabel = new Label
211203
{
212-
Text = job.Value >= 0 ?
204+
Text = job.Value != null ?
213205
Loc.GetString("late-join-gui-job-slot-capped", ("jobName", prototype.Name), ("amount", job.Value)) :
214206
Loc.GetString("late-join-gui-job-slot-uncapped", ("jobName", prototype.Name))
215207
};
@@ -234,8 +226,9 @@ private void RebuildUI()
234226
}
235227
}
236228

237-
private void JobsAvailableUpdated(IReadOnlyDictionary<StationId, Dictionary<string, int>> _)
229+
private void JobsAvailableUpdated(IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>> _)
238230
{
231+
Logger.Debug("UI rebuilt.");
239232
RebuildUI();
240233
}
241234

@@ -255,9 +248,9 @@ protected override void Dispose(bool disposing)
255248
sealed class JobButton : ContainerButton
256249
{
257250
public string JobId { get; }
258-
public int Amount { get; }
251+
public uint? Amount { get; }
259252

260-
public JobButton(string jobId, int amount)
253+
public JobButton(string jobId, uint? amount)
261254
{
262255
JobId = jobId;
263256
Amount = amount;
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Content.Server.Administration.Managers;
6+
using Content.Server.Maps;
7+
using Content.Server.Station.Systems;
8+
using Content.Shared.Preferences;
9+
using NUnit.Framework;
10+
using Robust.Server;
11+
using Robust.Shared.GameObjects;
12+
using Robust.Shared.Log;
13+
using Robust.Shared.Map;
14+
using Robust.Shared.Network;
15+
using Robust.Shared.Prototypes;
16+
using Robust.Shared.Timing;
17+
18+
namespace Content.IntegrationTests.Tests.Station;
19+
20+
[TestFixture]
21+
[TestOf(typeof(StationJobsSystem))]
22+
public sealed class StationJobsTest : ContentIntegrationTest
23+
{
24+
private const string Prototypes = @"
25+
- type: gameMap
26+
id: FooStation
27+
minPlayers: 0
28+
mapName: FooStation
29+
mapPath: Maps/Tests/empty.yml
30+
stations:
31+
Station:
32+
mapNameTemplate: FooStation
33+
overflowJobs:
34+
- Assistant
35+
availableJobs:
36+
TMime: [0, -1]
37+
TAssistant: [-1, -1]
38+
TCaptain: [5, 5]
39+
TClown: [5, 6]
40+
41+
- type: job
42+
id: TAssistant
43+
44+
- type: job
45+
id: TMime
46+
weight: 20
47+
48+
- type: job
49+
id: TClown
50+
weight: -10
51+
52+
- type: job
53+
id: TCaptain
54+
weight: 10
55+
56+
- type: job
57+
id: TChaplain
58+
";
59+
60+
private const int StationCount = 100;
61+
private const int CaptainCount = StationCount;
62+
private const int PlayerCount = 2000;
63+
private const int TotalPlayers = PlayerCount + CaptainCount;
64+
65+
[Test]
66+
public async Task AssignJobsTest()
67+
{
68+
var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes, Options = new ServerOptions() { LoadContentResources = false }};
69+
var server = StartServer(options);
70+
71+
await server.WaitIdleAsync();
72+
73+
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
74+
var mapManager = server.ResolveDependency<IMapManager>();
75+
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
76+
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
77+
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
78+
var stationSystem = entSysMan.GetEntitySystem<StationSystem>();
79+
80+
List<EntityUid> stations = new();
81+
await server.WaitPost(() =>
82+
{
83+
mapManager.CreateNewMapEntity(MapId.Nullspace);
84+
for (var i = 0; i < StationCount; i++)
85+
{
86+
stations.Add(stationSystem.InitializeNewStation(fooStationProto.Stations["Station"], null, $"Foo {StationCount}"));
87+
}
88+
});
89+
90+
await server.WaitAssertion(() =>
91+
{
92+
93+
var fakePlayers = new Dictionary<NetUserId, HumanoidCharacterProfile>()
94+
.AddJob("TAssistant", JobPriority.Medium, PlayerCount)
95+
.AddPreference("TClown", JobPriority.Low)
96+
.AddPreference("TMime", JobPriority.High)
97+
.WithPlayers(
98+
new Dictionary<NetUserId, HumanoidCharacterProfile>()
99+
.AddJob("TCaptain", JobPriority.High, CaptainCount)
100+
);
101+
102+
var start = new Stopwatch();
103+
start.Start();
104+
var assigned = stationJobs.AssignJobs(fakePlayers, stations);
105+
var time = start.Elapsed.TotalMilliseconds;
106+
Logger.Info($"Took {time} ms to distribute {TotalPlayers} players.");
107+
108+
foreach (var station in stations)
109+
{
110+
var assignedHere = assigned
111+
.Where(x => x.Value.Item2 == station)
112+
.ToDictionary(x => x.Key, x => x.Value);
113+
114+
// Each station should have SOME players.
115+
Assert.That(assignedHere, Is.Not.Empty);
116+
// And it should have at least the minimum players to be considered a "fair" share, as they're all the same.
117+
Assert.That(assignedHere, Has.Count.GreaterThanOrEqualTo(TotalPlayers/stations.Count), "Station has too few players.");
118+
// And it shouldn't have ALL the players, either.
119+
Assert.That(assignedHere, Has.Count.LessThan(TotalPlayers), "Station has too many players.");
120+
// And there should be *A* captain, as there's one player with captain enabled per station.
121+
Assert.That(assignedHere.Where(x => x.Value.Item1 == "TCaptain").ToList(), Has.Count.EqualTo(1));
122+
}
123+
124+
// All clown players have assistant as a higher priority.
125+
Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Not.Contain("TClown"));
126+
// Mime isn't an open job-slot at round-start.
127+
Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Not.Contain("TMime"));
128+
// All players have slots they can fill.
129+
Assert.That(assigned.Values, Has.Count.EqualTo(TotalPlayers), $"Expected {TotalPlayers} players.");
130+
// There must be assistants present.
131+
Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TAssistant"));
132+
// There must be captains present, too.
133+
Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TCaptain"));
134+
});
135+
}
136+
137+
[Test]
138+
public async Task AdjustJobsTest()
139+
{
140+
var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes, Options = new ServerOptions() { LoadContentResources = false }};
141+
var server = StartServer(options);
142+
143+
await server.WaitIdleAsync();
144+
145+
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
146+
var mapManager = server.ResolveDependency<IMapManager>();
147+
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
148+
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
149+
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
150+
var stationSystem = entSysMan.GetEntitySystem<StationSystem>();
151+
152+
var station = EntityUid.Invalid;
153+
await server.WaitPost(() =>
154+
{
155+
mapManager.CreateNewMapEntity(MapId.Nullspace);
156+
station = stationSystem.InitializeNewStation(fooStationProto.Stations["Station"], null, $"Foo Station");
157+
});
158+
159+
await server.WaitAssertion(() =>
160+
{
161+
// Verify jobs are/are not unlimited.
162+
Assert.Multiple(() =>
163+
{
164+
Assert.That(stationJobs.IsJobUnlimited(station, "TAssistant"), "TAssistant is expected to be unlimited.");
165+
Assert.That(stationJobs.IsJobUnlimited(station, "TMime"), "TMime is expected to be unlimited.");
166+
Assert.That(!stationJobs.IsJobUnlimited(station, "TCaptain"), "TCaptain is expected to not be unlimited.");
167+
Assert.That(!stationJobs.IsJobUnlimited(station, "TClown"), "TClown is expected to not be unlimited.");
168+
});
169+
Assert.Multiple(() =>
170+
{
171+
Assert.That(stationJobs.TrySetJobSlot(station, "TClown", 0), "Could not set TClown to have zero slots.");
172+
Assert.That(stationJobs.TryGetJobSlot(station, "TClown", out var clownSlots), "Could not get the number of TClown slots.");
173+
Assert.That(clownSlots, Is.EqualTo(0));
174+
Assert.That(!stationJobs.TryAdjustJobSlot(station, "TCaptain", -9999), "Was able to adjust TCaptain by -9999 without clamping.");
175+
Assert.That(stationJobs.TryAdjustJobSlot(station, "TCaptain", -9999, false, true), "Could not adjust TCaptain by -9999.");
176+
Assert.That(stationJobs.TryGetJobSlot(station, "TCaptain", out var captainSlots), "Could not get the number of TCaptain slots.");
177+
Assert.That(captainSlots, Is.EqualTo(0));
178+
});
179+
Assert.Multiple(() =>
180+
{
181+
Assert.That(stationJobs.TrySetJobSlot(station, "TChaplain", 10, true), "Could not create 10 TChaplain slots.");
182+
stationJobs.MakeJobUnlimited(station, "TChaplain");
183+
Assert.That(stationJobs.IsJobUnlimited(station, "TChaplain"), "Could not make TChaplain unlimited.");
184+
});
185+
});
186+
}
187+
}
188+
189+
internal static class JobExtensions
190+
{
191+
public static Dictionary<NetUserId, HumanoidCharacterProfile> AddJob(
192+
this Dictionary<NetUserId, HumanoidCharacterProfile> inp, string jobId, JobPriority prio = JobPriority.Medium,
193+
int amount = 1)
194+
{
195+
for (var i = 0; i < amount; i++)
196+
{
197+
inp.Add(new NetUserId(Guid.NewGuid()), HumanoidCharacterProfile.Random().WithJobPriority(jobId, prio));
198+
}
199+
200+
return inp;
201+
}
202+
203+
public static Dictionary<NetUserId, HumanoidCharacterProfile> AddPreference(
204+
this Dictionary<NetUserId, HumanoidCharacterProfile> inp, string jobId, JobPriority prio = JobPriority.Medium)
205+
{
206+
return inp.ToDictionary(x => x.Key, x => x.Value.WithJobPriority(jobId, prio));
207+
}
208+
209+
public static Dictionary<NetUserId, HumanoidCharacterProfile> WithPlayers(
210+
this Dictionary<NetUserId, HumanoidCharacterProfile> inp,
211+
Dictionary<NetUserId, HumanoidCharacterProfile> second)
212+
{
213+
return new[] {inp, second}.SelectMany(x => x).ToDictionary(x => x.Key, x => x.Value);
214+
}
215+
}

Content.Server/AI/Components/AiControllerComponent.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Content.Server.AI.EntitySystems;
2-
using Content.Server.GameTicking;
3-
using Content.Shared.ActionBlocker;
2+
using Content.Server.Station.Systems;
43
using Content.Shared.Movement.Components;
54
using Content.Shared.Roles;
65
using Robust.Shared.Map;
@@ -66,11 +65,11 @@ protected override void Startup()
6665

6766
if (StartingGearPrototype != null)
6867
{
69-
var gameTicker = EntitySystem.Get<GameTicker>();
68+
var stationSpawning = EntitySystem.Get<StationSpawningSystem>();
7069
var protoManager = IoCManager.Resolve<IPrototypeManager>();
7170

7271
var startingGear = protoManager.Index<StartingGearPrototype>(StartingGearPrototype);
73-
gameTicker.EquipStartingGear(Owner, startingGear, null);
72+
stationSpawning.EquipStartingGear(Owner, startingGear, null);
7473
}
7574
}
7675

Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
using Content.Server.Station;
1+
using Content.Server.Station.Systems;
22
using Content.Shared.Administration;
33
using Content.Shared.Roles;
4-
using Content.Shared.Station;
54
using Robust.Shared.Console;
6-
using Robust.Shared.GameObjects;
7-
using Robust.Shared.IoC;
8-
using Robust.Shared.Localization;
95
using Robust.Shared.Prototypes;
106

117
namespace Content.Server.Administration.Commands.Station;
@@ -27,16 +23,17 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
2723
return;
2824
}
2925

30-
26+
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
3127
var stationSystem = EntitySystem.Get<StationSystem>();
28+
var stationJobs = EntitySystem.Get<StationJobsSystem>();
3229

33-
if (!uint.TryParse(args[0], out var station) || !stationSystem.StationInfo.ContainsKey(new StationId(station)))
30+
if (!int.TryParse(args[0], out var stationInt) || !stationSystem.Stations.Contains(new EntityUid(stationInt)))
3431
{
3532
shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1)));
3633
return;
3734
}
3835

39-
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
36+
var station = new EntityUid(stationInt);
4037

4138
if (!prototypeManager.TryIndex<JobPrototype>(args[1], out var job))
4239
{
@@ -52,6 +49,12 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
5249
return;
5350
}
5451

55-
stationSystem.AdjustJobsAvailableOnStation(new StationId(station), job, amount);
52+
if (amount == -1)
53+
{
54+
stationJobs.MakeJobUnlimited(station, job);
55+
return;
56+
}
57+
58+
stationJobs.TrySetJobSlot(station, job, amount, true);
5659
}
5760
}

0 commit comments

Comments
 (0)