Add Job preference tests (#28625)

* Misc Job related changes

* Add JobTest

* A

* Aa

* Lets not confuse the yaml linter

* fixes

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

View File

@@ -4,10 +4,12 @@ using Content.Client.Lobby;
using Content.Client.RoundEnd;
using Content.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
}

View File

@@ -290,7 +290,7 @@ namespace Content.Client.LateJoin
}
}
private void JobsAvailableUpdated(IReadOnlyDictionary<NetEntity, Dictionary<string, uint?>> updatedJobs)
private void JobsAvailableUpdated(IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> updatedJobs)
{
foreach (var stationEntries in updatedJobs)
{
@@ -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)
{

View 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)

View 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}";
}
}

View 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;
public async Task SetAntagPref(NetUserId user, ProtoId<AntagPrototype> id, bool value)
{
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(userId);
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;
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
Assert.That(profile.AntagPreferences.Contains(id), Is.EqualTo(!value));
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
}
await Server.WaitPost(() =>
/// <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)
{
prefMan.SetProfile(user, prefs.SelectedCharacterIndex, newProfile).Wait();
});
user ??= Client.User!.Value;
if (user is { } userId)
await SetJobPriorities(userId, (id, value));
}
// 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));
/// <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);
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
if (highCount != 0)
{
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;
}
var newProfile = profile.WithJobPriorities(dictionary);
_modifiedProfiles.Add(user);
await Server.WaitPost(() => prefMan.SetProfile(user, 0, newProfile).Wait());
}
}

View File

@@ -2,10 +2,12 @@
using System.IO;
using System.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)

View 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();

View 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"),

View 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.

View 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);

View 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();
}
}

View 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 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);
}
var comp = entManager.GetComponent<StationJobsComponent>(station);
var jobs = new HashSet<ProtoId<JobPrototype>>(comp.SetupAvailableJobs.Keys);
Assert.That(missingSpawnPoints, Has.Count.EqualTo(0),
$"There is no spawnpoint for {string.Join(", ", missingSpawnPoints)} on {mapProto}.");
var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
.Where(x => x.SpawnType == SpawnPointType.Job)
.Select(x => x.Job!.Value);
jobs.ExceptWith(spawnPoints);
Assert.That(jobs, Is.Empty,$"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
}
try

View File

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

View File

@@ -7,7 +7,6 @@ using Content.Shared.Preferences;
using Content.Shared.Roles;
using 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.");
}
}

View 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))

View 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
{

View 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)

View 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}";
}
}

View 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);
}

View 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!;
}

View File

@@ -52,23 +52,23 @@ public sealed partial class StationJobsSystem
/// as there may end up being more round-start slots than available slots, which can cause weird behavior.
/// 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)

View 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.JobList = stationJobs.SetupAvailableJobs.ToDictionary(
x => x.Key,
x=> (int?)(x.Value[1] < 0 ? null : x.Value[1]));
stationJobs.RoundStartTotalJobs = mapJobList.Values.Where(x => x[0] is not null && x[0] > 0).Sum(x => x[0]!.Value);
stationJobs.MidRoundTotalJobs = mapJobList.Values.Where(x => x[1] is not null && x[1] > 0).Sum(x => x[1]!.Value);
stationJobs.TotalJobs = stationJobs.MidRoundTotalJobs;
stationJobs.JobList = mapJobList.ToDictionary(x => x.Key, x =>
{
if (x.Value[1] <= -1)
return null;
return (uint?) x.Value[1];
});
stationJobs.RoundStartJobList = mapJobList.ToDictionary(x => x.Key, x =>
{
if (x.Value[0] <= -1)
return null;
return (uint?) x.Value[0];
});
stationJobs.OverflowJobs = stationJobs.OverflowJobs.ToHashSet();
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>();

View 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]

View 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();

View 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; }

View 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.

View File

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

View 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 ]

View File

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

View File

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

View File

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

View File

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

View 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 ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 ]