From 6849be2d9cf3c64e066bebc3e9facccf2fa62660 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+electrojr@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:19:24 +0300 Subject: [PATCH] Add Job preference tests (#28625) --- .../GameTicking/Managers/ClientGameTicker.cs | 14 +- Content.Client/LateJoin/LateJoinGui.cs | 8 +- Content.Client/Lobby/LobbyUIController.cs | 3 +- .../Lobby/UI/CharacterPickerButton.xaml.cs | 4 +- .../_EE/Contractors/Systems/PassportSystem.cs | 2 +- .../Pair/TestPair.Helpers.cs | 74 ++++-- .../Pair/TestPair.Recycle.cs | 11 + Content.IntegrationTests/Pair/TestPair.cs | 7 +- Content.IntegrationTests/PoolManager.Cvars.cs | 1 + Content.IntegrationTests/Tests/EntityTest.cs | 1 + .../Tests/GameRules/AntagPreferenceTest.cs | 4 +- .../Tests/GameRules/NukeOpsTest.cs | 4 +- .../Tests/PostMapInitTest.cs | 4 +- .../Tests/Round/JobTest.cs | 222 ++++++++++++++++++ .../Tests/Station/StationJobsTest.cs | 13 +- Content.Server/Database/ServerDbBase.cs | 7 +- .../GameTicking/GameTicker.RoundFlow.cs | 2 +- .../Managers/ServerPreferencesManager.cs | 6 +- .../Components/SpawnPointComponent.cs | 9 +- .../EntitySystems/SpawnPointSystem.cs | 4 +- .../Station/Commands/JobsCommand.cs | 4 +- .../Components/StationJobsComponent.cs | 46 ++-- .../Systems/StationJobsSystem.Roundstart.cs | 23 +- .../Station/Systems/StationJobsSystem.cs | 138 ++++++----- .../GameTicking/SharedGameTicker.cs | 15 +- .../Preferences/HumanoidCharacterProfile.cs | 106 +++++++-- .../Prototypes/CharacterItemGroupPrototype.cs | 7 +- Content.Shared/Roles/JobPrototype.cs | 7 +- Resources/Prototypes/Maps/Kettle.yml | 4 +- Resources/Prototypes/Maps/Molecule.yml | 2 - Resources/Prototypes/Maps/WhiteMeta.yml | 4 +- Resources/Prototypes/Maps/Wonderbox.yml | 2 - Resources/Prototypes/Maps/anchor.yml | 2 - Resources/Prototypes/Maps/arena.yml | 2 - Resources/Prototypes/Maps/arenas.yml | 2 - Resources/Prototypes/Maps/asterisk.yml | 2 - Resources/Prototypes/Maps/core.yml | 2 - Resources/Prototypes/Maps/debug.yml | 6 - Resources/Prototypes/Maps/drydock.yml | 2 - Resources/Prototypes/Maps/edge.yml | 2 - Resources/Prototypes/Maps/europa.yml | 2 - Resources/Prototypes/Maps/glacier.yml | 2 - Resources/Prototypes/Maps/hammurabi.yml | 2 - Resources/Prototypes/Maps/hive.yml | 2 - Resources/Prototypes/Maps/lighthouse.yml | 2 - Resources/Prototypes/Maps/moose.yml | 2 - Resources/Prototypes/Maps/northway.yml | 2 - Resources/Prototypes/Maps/pebble.yml | 120 +++++----- Resources/Prototypes/Maps/radstation.yml | 2 - Resources/Prototypes/Maps/saltern.yml | 2 - Resources/Prototypes/Maps/shoukou.yml | 2 - Resources/Prototypes/Maps/submarine.yml | 2 - Resources/Prototypes/Maps/tortuga.yml | 2 - Resources/Prototypes/Maps/whitebox.yml | 2 - 54 files changed, 592 insertions(+), 332 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Round/JobTest.cs diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index 194d337af5..c617ae0043 100644 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs @@ -5,10 +5,12 @@ using Content.Client.RoundEnd; using Content.Shared._White.GameTicking.Prototypes; 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 { @@ -19,10 +21,8 @@ namespace Content.Client.GameTicking.Managers [Dependency] private readonly IClientAdminManager _admin = default!; [Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; -#if !DEBUG - [Dependency] private readonly SharedMapSystem _map = default!; -#endif - private Dictionary> _jobsAvailable = new(); + + private Dictionary, int?>> _jobsAvailable = new(); private Dictionary _stationNames = new(); [ViewVariables] public bool AreWeReady { get; private set; } @@ -34,13 +34,13 @@ namespace Content.Client.GameTicking.Managers [ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public new bool Paused { get; private set; } - [ViewVariables] public IReadOnlyDictionary> JobsAvailable => _jobsAvailable; + [ViewVariables] public IReadOnlyDictionary, int?>> JobsAvailable => _jobsAvailable; [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; public event Action? InfoBlobUpdated; public event Action? LobbyStatusUpdated; public event Action? LobbyLateJoinStatusUpdated; - public event Action>>? LobbyJobsAvailableUpdated; + public event Action, int?>>>? LobbyJobsAvailableUpdated; public override void Initialize() { @@ -71,7 +71,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().Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning; #endif } diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index b7bc49d8b7..d273f2480a 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -325,7 +325,7 @@ namespace Content.Client.LateJoin } } - private void JobsAvailableUpdated(IReadOnlyDictionary> updatedJobs) + private void JobsAvailableUpdated(IReadOnlyDictionary, int?>> updatedJobs) { foreach (var stationEntries in updatedJobs) { @@ -372,10 +372,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 jobId, string jobLocalisedName, int? amount) { JobLabel = jobLabel; JobId = jobId; @@ -385,7 +385,7 @@ namespace Content.Client.LateJoin _initialised = true; } - public void RefreshLabel(uint? amount) + public void RefreshLabel(int? amount) { if (Amount == amount && _initialised) { diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs index 26c1d1e674..f63b6cd586 100644 --- a/Content.Client/Lobby/LobbyUIController.cs +++ b/Content.Client/Lobby/LobbyUIController.cs @@ -257,7 +257,8 @@ public sealed class LobbyUIController : UIController, IOnStateEntered p.Value == JobPriority.High).Key; - return _prototypeManager.Index(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob); + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?) + return _prototypeManager.Index(highPriorityJob.Id ?? SharedGameTicker.FallbackOverflowJob); } public void RemoveDummyClothes(EntityUid dummy) diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs b/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs index 8b81da0693..55921fcaae 100644 --- a/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs +++ b/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs @@ -49,9 +49,9 @@ public sealed partial class CharacterPickerButton : ContainerButton .LoadProfileEntity(humanoid, true, true); var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key; - if (highPriorityJob != null) + if (highPriorityJob != default) { - var jobName = prototypeManager.Index(highPriorityJob).LocalizedName; + var jobName = prototypeManager.Index(highPriorityJob).LocalizedName; description = $"{description}\n{jobName}"; } } diff --git a/Content.Client/_EE/Contractors/Systems/PassportSystem.cs b/Content.Client/_EE/Contractors/Systems/PassportSystem.cs index 86a46dc797..96a8388ee3 100644 --- a/Content.Client/_EE/Contractors/Systems/PassportSystem.cs +++ b/Content.Client/_EE/Contractors/Systems/PassportSystem.cs @@ -33,7 +33,7 @@ public sealed class PassportSystem : EntitySystem if (currentState.Name == null) return; - sprite.LayerSetState(1, currentState.Name.Replace("human", profile.Species.ToLower())); + sprite.LayerSetState(1, currentState.Name.Replace("human", profile.Species.ToString().ToLower())); } private void OnPassportToggled(Entity passport, ref SharedPassportSystem.PassportToggleEvent evt) diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs index 7f13e5b86e..588cf0d80e 100644 --- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs +++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs @@ -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 } /// - /// 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. /// - public async Task SetAntagPref(ProtoId id, bool value) + public async Task SetAntagPreference(ProtoId id, bool value, NetUserId? user = null) { - await SetAntagPref(Client.User!.Value, id, value); + user ??= Client.User!.Value; + if (user is not {} userId) + return; + + var prefMan = Server.ResolveDependency(); + var prefs = prefMan.GetPreferences(userId); + + // Automatic preference resetting only resets slot 0. + Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0)); + + var profile = (HumanoidCharacterProfile) prefs.Characters[0]; + var newProfile = profile.WithAntagPreference(id, value); + _modifiedProfiles.Add(userId); + await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait()); } - public async Task SetAntagPref(NetUserId user, ProtoId id, bool value) + /// + /// Set a user's job preferences. Modified preferences are automatically reset at the end of the test. + /// + public async Task SetJobPriority(ProtoId id, JobPriority value, NetUserId? user = null) { + user ??= Client.User!.Value; + if (user is { } userId) + await SetJobPriorities(userId, (id, value)); + } + + /// + public async Task SetJobPriorities(params (ProtoId, JobPriority)[] priorities) + => await SetJobPriorities(Client.User!.Value, priorities); + + /// + public async Task SetJobPriorities(NetUserId user, params (ProtoId, 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(); - var prefs = prefMan.GetPreferences(user); - // what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable? - var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter; + var profile = (HumanoidCharacterProfile) prefs.Characters[0]; + var dictionary = new Dictionary, JobPriority>(profile.JobPriorities); - Assert.That(profile.AntagPreferences.Any(preference => preference == id), Is.EqualTo(!value)); - var newProfile = profile.WithAntagPreference(id, value); + // Automatic preference resetting only resets slot 0. + Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0)); - await Server.WaitPost(() => + if (highCount != 0) { - prefMan.SetProfile(user, prefs.SelectedCharacterIndex, newProfile).Wait(); - }); + foreach (var (key, priority) in dictionary) + { + if (priority == JobPriority.High) + dictionary[key] = JobPriority.Medium; + } + } - // 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.Any(preference => preference == id), Is.EqualTo(value)); + 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()); } } diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs index b67b106de7..89a9eb6463 100644 --- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs +++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs @@ -38,6 +38,7 @@ public sealed partial class TestPair : IAsyncDisposable { await Server.WaitIdleAsync(); await Client.WaitIdleAsync(); + await ResetModifiedPreferences(); await Server.RemoveAllDummySessions(); if (TestMap != null) @@ -85,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(); + 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) diff --git a/Content.IntegrationTests/Pair/TestPair.cs b/Content.IntegrationTests/Pair/TestPair.cs index 30ac2bc3cd..43b188fd32 100644 --- a/Content.IntegrationTests/Pair/TestPair.cs +++ b/Content.IntegrationTests/Pair/TestPair.cs @@ -27,7 +27,8 @@ public sealed partial class TestPair public readonly List TestHistory = new(); public PoolSettings Settings = default!; public TestMapData? TestMap; - + private List _modifiedProfiles = new(); + private int _nextServerSeed; private int _nextClientSeed; @@ -45,9 +46,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(); diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs index 8d65dd69ed..23f0ded7df 100644 --- a/Content.IntegrationTests/PoolManager.Cvars.cs +++ b/Content.IntegrationTests/PoolManager.Cvars.cs @@ -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"), diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index 8256fbaa80..7c395a76fc 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -352,6 +352,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. diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs index 889c7868d7..b215584c57 100644 --- a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs @@ -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); diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index 09b4274a7f..7e385fb6fc 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -65,8 +65,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); diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index eb068ed5cc..0d26b7bbae 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -263,13 +263,13 @@ namespace Content.IntegrationTests.Tests } var comp = entManager.GetComponent(station); - var jobs = new HashSet(comp.SetupAvailableJobs.Keys); + var jobs = new HashSet>(comp.SetupAvailableJobs.Keys); // Test all availableJobs have spawnPoints // This is done inside gamemap test because loading the map takes ages and we already have it. var spawnPoints = entManager.EntityQuery() .Where(x => x.SpawnType == SpawnPointType.Job) - .Select(x => x.Job!.ID); + .Select(x => x.Job!.Value); jobs.ExceptWith(spawnPoints); diff --git a/Content.IntegrationTests/Tests/Round/JobTest.cs b/Content.IntegrationTests/Tests/Round/JobTest.cs new file mode 100644 index 0000000000..716e3cf4c2 --- /dev/null +++ b/Content.IntegrationTests/Tests/Round/JobTest.cs @@ -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 _passenger = "Passenger"; + private static ProtoId _engineer = "StationEngineer"; + private static ProtoId _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 job, NetUserId? user = null, bool isAntag = false) + { + var jobSys = pair.Server.System(); + var mindSys = pair.Server.System(); + var roleSys = pair.Server.System(); + var ticker = pair.Server.System(); + + 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)); + } + + /// + /// Simple test that checks that starting the round spawns the player into the test map as a passenger. + /// + [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(); + + // 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(); + } + + /// + /// Check that job preferences are respected. + /// + [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(); + 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(); + } + + /// + /// Check high priority jobs (e.g., captain) are selected before other roles, even if it means a player does not + /// get their preferred job. + /// + [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(); + 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(); + } + + /// + /// Check that jobs are preferentially given to players that have marked those jobs as higher priority. + /// + [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(); + 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(); + } +} diff --git a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs index 0085472c33..d68fdafb76 100644 --- a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs +++ b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs @@ -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(); - var mapManager = server.ResolveDependency(); var fooStationProto = prototypeManager.Index("FooStation"); var entSysMan = server.ResolveDependency().EntitySysManager; var stationJobs = entSysMan.GetEntitySystem(); @@ -215,6 +211,8 @@ public sealed class StationJobsTest var server = pair.Server; var prototypeManager = server.ResolveDependency(); + var compFact = server.ResolveDependency(); + var name = compFact.GetComponentName(); 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."); } } diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index 9c0b4a0758..7d2e93827b 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -19,6 +19,7 @@ using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Utility; using Content.Shared.Roles; +using Content.Shared.Traits; using Robust.Shared.Prototypes; namespace Content.Server.Database @@ -180,9 +181,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(j.JobName), j => (JobPriority) j.Priority); + var antags = profile.Antags.Select(a => new ProtoId(a.AntagName)); + var traits = profile.Traits.Select(t => new ProtoId(t.TraitName)); var loadouts = profile.Loadouts.Select(Shared.Clothing.Loadouts.Systems.Loadout (l) => l); var sex = Sex.Male; diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 89b8407acc..b73634fb00 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -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 { diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index ac280dac0a..a7e053e544 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -318,11 +318,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(p.p, p.Prefs!.GetProfile(idx)); - }); + .Select(p => new KeyValuePair(p.p, p.Prefs!.SelectedCharacter)); } internal static bool ShouldStorePrefs(LoginType loginType) diff --git a/Content.Server/Spawners/Components/SpawnPointComponent.cs b/Content.Server/Spawners/Components/SpawnPointComponent.cs index 5cf231f224..c6d14dfeb3 100644 --- a/Content.Server/Spawners/Components/SpawnPointComponent.cs +++ b/Content.Server/Spawners/Components/SpawnPointComponent.cs @@ -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? Job; /// /// 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(_jobId); - public override string ToString() { - return $"{_jobId} {SpawnType}"; + return $"{Job} {SpawnType}"; } } diff --git a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs b/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs index 831ed6a491..177da007c6 100644 --- a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs +++ b/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs @@ -36,7 +36,7 @@ public sealed class SpawnPointSystem : EntitySystem if (args.DesiredSpawnPointType != SpawnPointType.Unset) { var isMatchingJob = spawnPoint.SpawnType == SpawnPointType.Job && - (args.Job == null || spawnPoint.Job?.ID == args.Job); + (args.Job == null || spawnPoint.Job == args.Job); switch (args.DesiredSpawnPointType) { @@ -57,7 +57,7 @@ public sealed class SpawnPointSystem : EntitySystem if (_gameTicker.RunLevel != GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.Job && - (args.Job == null || spawnPoint.Job is not null && spawnPoint.Job == args.Job)) + (args.Job == null || spawnPoint.Job == args.Job)) { possiblePositions.Add(xform.Coordinates); } diff --git a/Content.Server/Station/Commands/JobsCommand.cs b/Content.Server/Station/Commands/JobsCommand.cs index 3f26090828..4af1a024d5 100644 --- a/Content.Server/Station/Commands/JobsCommand.cs +++ b/Content.Server/Station/Commands/JobsCommand.cs @@ -70,7 +70,7 @@ public sealed class JobsCommand : ToolshedCommand public IEnumerable Set([PipedArgument] IEnumerable @ref, int by) => @ref.Select(x => Set(x, by)); [CommandImplementation("amount")] - public uint Amount([PipedArgument] JobSlotRef @ref) + public int Amount([PipedArgument] JobSlotRef @ref) { _jobs ??= GetSys(); _jobs.TryGetJobSlot(@ref.Station, @ref.Job, out var slots); @@ -78,7 +78,7 @@ public sealed class JobsCommand : ToolshedCommand } [CommandImplementation("amount")] - public IEnumerable Amount([PipedArgument] IEnumerable @ref) => @ref.Select(Amount); + public IEnumerable Amount([PipedArgument] IEnumerable @ref) => @ref.Select(Amount); } // Used for Toolshed queries. diff --git a/Content.Server/Station/Components/StationJobsComponent.cs b/Content.Server/Station/Components/StationJobsComponent.cs index 74399bf412..3681ec9674 100644 --- a/Content.Server/Station/Components/StationJobsComponent.cs +++ b/Content.Server/Station/Components/StationJobsComponent.cs @@ -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 { - /// - /// Total *round-start* jobs at station start. - /// - [DataField("roundStartTotalJobs")] public int RoundStartTotalJobs; - /// /// Total *mid-round* jobs at station start. + /// This is inferred automatically from . /// - [DataField("midRoundTotalJobs")] public int MidRoundTotalJobs; + [ViewVariables] public int MidRoundTotalJobs; /// /// Current total jobs. /// - [DataField("totalJobs")] public int TotalJobs; + [DataField] public int TotalJobs; /// /// Station is running on extended access. /// - [DataField("extendedAccess")] public bool ExtendedAccess; + [DataField] public bool ExtendedAccess; /// /// 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 /// /// Set to -1 to disable extended access. /// - [DataField("extendedAccessThreshold")] + [DataField] public int ExtendedAccessThreshold { get; set; } = 15; /// @@ -54,28 +51,20 @@ public sealed partial class StationJobsComponent : Component public float? PercentJobsRemaining => MidRoundTotalJobs > 0 ? TotalJobs / (float) MidRoundTotalJobs : null; /// - /// The current list of jobs. + /// The current list of jobs of available jobs. Null implies that is no limit. /// /// /// This should not be mutated or used directly unless you really know what you're doing, go through StationJobsSystem. /// - [DataField("jobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] - public Dictionary JobList = new(); - - /// - /// The round-start list of jobs. - /// - /// - /// This should not be mutated, ever. - /// - [DataField("roundStartJobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] - public Dictionary RoundStartJobList = new(); + [DataField] + public Dictionary, int?> JobList = new(); /// /// Overflow jobs that round-start can spawn infinitely many of. + /// This is inferred automatically from . /// - [DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] - public HashSet OverflowJobs = new(); + [ViewVariables] + public IReadOnlySet> OverflowJobs = default!; /// /// 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>> PlayerJobs = new(); - [DataField("availableJobs", required: true, - customTypeSerializer: typeof(PrototypeIdDictionarySerializer, JobPrototype>))] - public Dictionary> SetupAvailableJobs = default!; + /// + /// 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. + /// + [DataField("availableJobs", required: true)] + public Dictionary, int[]> SetupAvailableJobs = default!; } diff --git a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs index c3c3865c7b..e145e233e9 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs +++ b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs @@ -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. /// - public Dictionary AssignJobs(Dictionary profiles, IReadOnlyList stations, bool useRoundStartJobs = true) + public Dictionary?, EntityUid)> AssignJobs(Dictionary profiles, IReadOnlyList stations, bool useRoundStartJobs = true) { DebugTools.Assert(stations.Count > 0); InitializeRoundStart(); if (profiles.Count == 0) - return new Dictionary(); + 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(profiles.Count); + var assigned = new Dictionary?, 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>(); + var stationJobs = new Dictionary, 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>(stations.Count); + var currentlySelectingJobs = new Dictionary, int?>>(stations.Count); foreach (var station in stations) { - currentlySelectingJobs.Add(station, new Dictionary()); + currentlySelectingJobs.Add(station, new Dictionary, int?>()); } // And these. // Tracks what players are available for a given job in the current iteration of selection. - var jobPlayerOptions = new Dictionary>(); + var jobPlayerOptions = new Dictionary, HashSet>(); // Tracks the total number of slots for the given stations in the current iteration of selection. var stationTotalSlots = new Dictionary(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 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 /// All players that might need an overflow assigned. /// Player character profiles. /// The stations to consider for spawn location. - public void AssignOverflowJobs(ref Dictionary assignedJobs, - IEnumerable allPlayersToAssign, IReadOnlyDictionary profiles, IReadOnlyList stations) + public void AssignOverflowJobs( + ref Dictionary?, EntityUid)> assignedJobs, + IEnumerable allPlayersToAssign, + IReadOnlyDictionary profiles, + IReadOnlyList stations) { var givenStations = stations.ToList(); if (givenStations.Count == 0) diff --git a/Content.Server/Station/Systems/StationJobsSystem.cs b/Content.Server/Station/Systems/StationJobsSystem.cs index f708e8526d..4ec5b2ab12 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.cs +++ b/Content.Server/Station/Systems/StationJobsSystem.cs @@ -4,6 +4,7 @@ using Content.Server.DeltaV.Station.Events; // DeltaV 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; @@ -32,12 +33,25 @@ public sealed partial class StationJobsSystem : EntitySystem public override void Initialize() { SubscribeLocalEvent(OnStationInitialized); + SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnStationRenamed); SubscribeLocalEvent(OnStationDeletion); SubscribeLocalEvent(OnPlayerJoinedLobby); Subs.CVar(_configurationManager, CCVars.GameDisallowLateJoins, _ => UpdateJobsAvailable(), true); } + private void OnInit(Entity 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) @@ -58,28 +72,11 @@ public sealed partial class StationJobsSystem : EntitySystem if (!TryComp(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(); } @@ -143,7 +140,11 @@ public sealed partial class StationJobsSystem : EntitySystem /// Resolve pattern, station jobs component of the station. /// Whether or not slot adjustment was a success. /// Thrown when the given station is not a station. - 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)) @@ -158,7 +159,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; @@ -166,31 +171,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; } @@ -248,7 +242,10 @@ public sealed partial class StationJobsSystem : EntitySystem /// Resolve pattern, station jobs component of the station. /// Whether or not setting the value succeeded. /// Thrown when the given station is not a station. - 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)) @@ -264,13 +261,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; } @@ -298,8 +295,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; @@ -328,8 +325,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; } /// @@ -337,7 +333,7 @@ public sealed partial class StationJobsSystem : EntitySystem /// Job to get slot info for. /// The number of slots remaining. Null if infinite. /// Resolve pattern, station jobs component of the station. - 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); } @@ -352,21 +348,12 @@ public sealed partial class StationJobsSystem : EntitySystem /// Whether or not the slot exists. /// Thrown when the given station is not a station. /// slots will be null if the slot doesn't exist, as well, so make sure to check the return value. - 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); } /// @@ -376,12 +363,14 @@ public sealed partial class StationJobsSystem : EntitySystem /// Resolve pattern, station jobs component of the station. /// Set containing all jobs available. /// Thrown when the given station is not a station. - public IReadOnlySet GetAvailableJobs(EntityUid station, StationJobsComponent? stationJobs = null) + public IEnumerable> 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); } /// @@ -391,12 +380,12 @@ public sealed partial class StationJobsSystem : EntitySystem /// Resolve pattern, station jobs component of the station. /// Set containing all overflow jobs available. /// Thrown when the given station is not a station. - public IReadOnlySet GetOverflowJobs(EntityUid station, StationJobsComponent? stationJobs = null) + public IReadOnlySet> 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; } /// @@ -406,7 +395,7 @@ public sealed partial class StationJobsSystem : EntitySystem /// Resolve pattern, station jobs component of the station. /// List of all jobs on the station. /// Thrown when the given station is not a station. - public IReadOnlyDictionary GetJobs(EntityUid station, StationJobsComponent? stationJobs = null) + public IReadOnlyDictionary, 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)); @@ -421,12 +410,14 @@ public sealed partial class StationJobsSystem : EntitySystem /// Resolve pattern, station jobs component of the station. /// List of all round-start jobs. /// Thrown when the given station is not a station. - public IReadOnlyDictionary GetRoundStartJobs(EntityUid station, StationJobsComponent? stationJobs = null) + public Dictionary, 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])); } /// @@ -437,13 +428,13 @@ public sealed partial class StationJobsSystem : EntitySystem /// Whether or not to pick from the overflow list. /// A set of disallowed jobs, if any. /// The selected job, if any. - public string? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary jobPriorities, bool pickOverflows, IReadOnlySet>? disallowedJobs = null) + public ProtoId? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet>? 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? jobId) { var filtered = jobPriorities .Where(p => @@ -483,7 +474,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 @@ -492,7 +486,7 @@ public sealed partial class StationJobsSystem : EntitySystem private bool _availableJobsDirty; - private TickerJobsAvailableEvent _cachedAvailableJobs = new (new Dictionary(), new Dictionary>()); + private TickerJobsAvailableEvent _cachedAvailableJobs = new(new(), new()); /// /// Assembles an event from the current available-to-play jobs. @@ -503,9 +497,9 @@ public sealed partial class StationJobsSystem : EntitySystem { // If late join is disallowed, return no available jobs. if (_gameTicker.DisallowLateJoin) - return new TickerJobsAvailableEvent(new Dictionary(), new Dictionary>()); + return new TickerJobsAvailableEvent(new(), new()); - var jobs = new Dictionary>(); + var jobs = new Dictionary, int?>>(); var stationNames = new Dictionary(); var query = EntityQueryEnumerator(); diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs index 196168370c..1a3775b470 100644 --- a/Content.Shared/GameTicking/SharedGameTicker.cs +++ b/Content.Shared/GameTicking/SharedGameTicker.cs @@ -1,6 +1,7 @@ using Content.Shared._White.GameTicking.Prototypes; 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; @@ -134,19 +135,17 @@ namespace Content.Shared.GameTicking } [Serializable, NetSerializable] - public sealed class TickerJobsAvailableEvent : EntityEventArgs + public sealed class TickerJobsAvailableEvent( + Dictionary stationNames, + Dictionary, int?>> jobsAvailableByStation) + : EntityEventArgs { /// /// The Status of the Player in the lobby (ready, observer, ...) /// - public Dictionary> JobsAvailableByStation { get; } - public Dictionary StationNames { get; } + public Dictionary, int?>> JobsAvailableByStation { get; } = jobsAvailableByStation; - public TickerJobsAvailableEvent(Dictionary stationNames, Dictionary> jobsAvailableByStation) - { - StationNames = stationNames; - JobsAvailableByStation = jobsAvailableByStation; - } + public Dictionary StationNames { get; } = stationNames; } [Serializable, NetSerializable, DataDefinition] diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index a29f140696..26cc2d5763 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -32,7 +32,7 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile /// Job preferences for initial spawn [DataField] - private Dictionary _jobPriorities = new() + private Dictionary, JobPriority> _jobPriorities = new() { { SharedGameTicker.FallbackOverflowJob, JobPriority.High @@ -41,11 +41,11 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile /// Antags we have opted in to [DataField] - private HashSet _antagPreferences = new(); + private HashSet> _antagPreferences = new(); /// Enabled traits [DataField] - private HashSet _traitPreferences = new(); + private HashSet> _traitPreferences = new(); /// public HashSet LoadoutPreferences => _loadoutPreferences; @@ -62,7 +62,7 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile /// Associated for this profile [DataField] - public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies; + public ProtoId Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies; // EE -- Contractors Change Start [DataField] @@ -132,13 +132,13 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None; /// - public IReadOnlyDictionary JobPriorities => _jobPriorities; + public IReadOnlyDictionary, JobPriority> JobPriorities => _jobPriorities; /// - public IReadOnlySet AntagPreferences => _antagPreferences; + public IReadOnlySet> AntagPreferences => _antagPreferences; /// - public IReadOnlySet TraitPreferences => _traitPreferences; + public IReadOnlySet> TraitPreferences => _traitPreferences; /// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby [DataField] @@ -168,12 +168,12 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile string? clownName, // WD EDIT HumanoidCharacterAppearance appearance, SpawnPriorityPreference spawnPriority, - Dictionary jobPriorities, + Dictionary, JobPriority> jobPriorities, ClothingPreference clothing, BackpackPreference backpack, PreferenceUnavailableMode preferenceUnavailable, - HashSet antagPreferences, - HashSet traitPreferences, + HashSet> antagPreferences, + HashSet> traitPreferences, HashSet loadoutPreferences) { Name = name; @@ -205,6 +205,20 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile _antagPreferences = antagPreferences; _traitPreferences = traitPreferences; _loadoutPreferences = loadoutPreferences; + + 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; + } } /// Copy constructor @@ -232,12 +246,12 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile other.ClownName, // WD EDIT other.Appearance.Clone(), other.SpawnPriority, - new Dictionary(other.JobPriorities), + new Dictionary, JobPriority>(other.JobPriorities), other.Clothing, other.Backpack, other.PreferenceUnavailable, - new HashSet(other.AntagPreferences), - new HashSet(other.TraitPreferences), + new HashSet>(other.AntagPreferences), + new HashSet>(other.TraitPreferences), new HashSet(other.LoadoutPreferences)) { } @@ -379,14 +393,47 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile new(this) { Backpack = backpack }; public HumanoidCharacterProfile WithSpawnPriorityPreference(SpawnPriorityPreference spawnPriority) => new(this) { SpawnPriority = spawnPriority }; - public HumanoidCharacterProfile WithJobPriorities(IEnumerable> jobPriorities) => - new(this) { _jobPriorities = new Dictionary(jobPriorities) }; - public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority) + public HumanoidCharacterProfile WithJobPriorities(IEnumerable, JobPriority>> jobPriorities) { - var dictionary = new Dictionary(_jobPriorities); + var dictionary = new Dictionary, 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 = dictionary + }; + } + + public HumanoidCharacterProfile WithJobPriority(ProtoId jobId, JobPriority priority) + { + var dictionary = new Dictionary, 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; @@ -395,12 +442,12 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode) => new(this) { PreferenceUnavailable = mode }; - public HumanoidCharacterProfile WithAntagPreferences(IEnumerable antagPreferences) => - new(this) { _antagPreferences = new HashSet(antagPreferences) }; + public HumanoidCharacterProfile WithAntagPreferences(IEnumerable> antagPreferences) => + new(this) { _antagPreferences = new HashSet>(antagPreferences) }; - public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref) + public HumanoidCharacterProfile WithAntagPreference(ProtoId antagId, bool pref) { - var list = new HashSet(_antagPreferences); + var list = new HashSet>(_antagPreferences); if (pref) list.Add(antagId); else @@ -409,9 +456,9 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile return new(this) { _antagPreferences = list }; } - public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref) + public HumanoidCharacterProfile WithTraitPreference(ProtoId traitId, bool pref) { - var list = new HashSet(_traitPreferences); + var list = new HashSet>(_traitPreferences); if (pref) list.Add(traitId); @@ -576,7 +623,7 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile _ => SpawnPriorityPreference.None // Invalid enum values. }; - var priorities = new Dictionary(JobPriorities + var priorities = new Dictionary, JobPriority>(JobPriorities .Where(p => prototypeManager.TryIndex(p.Key, out var job) && job.SetPreference && p.Value switch { JobPriority.Never => false, // Drop never since that's assumed default. @@ -586,6 +633,17 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile _ => 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(id, out var antag) && antag.SetPreference) .Distinct() diff --git a/Content.Shared/Prototypes/CharacterItemGroupPrototype.cs b/Content.Shared/Prototypes/CharacterItemGroupPrototype.cs index 04e10e62a8..daf809a166 100644 --- a/Content.Shared/Prototypes/CharacterItemGroupPrototype.cs +++ b/Content.Shared/Prototypes/CharacterItemGroupPrototype.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Shared.Clothing.Loadouts.Prototypes; using Content.Shared.Clothing.Loadouts.Systems; using Content.Shared.Preferences; @@ -42,8 +43,10 @@ public sealed partial class CharacterItemGroupItem switch (Type) { case "trait": - return profile.TraitPreferences.TryFirstOrDefault( - p => protoMan.Index((string) p).ID == ID, out value); + // RobustToolbox my beloved + value = profile.TraitPreferences.FirstOrNull( + p => protoMan.Index((string) p).ID == ID); + return value != null; case "loadout": return profile.LoadoutPreferences.TryFirstOrDefault( p => protoMan.Index(((Loadout) p).LoadoutName).ID == ID, out value); diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index 8ee57a2465..1ed2c0f64f 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -81,8 +81,8 @@ namespace Content.Shared.Roles public bool AlwaysUseSpawner { get; } = false; /// - /// 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. /// [DataField("weight")] public int Weight { get; private set; } @@ -117,7 +117,7 @@ namespace Content.Shared.Roles /// /// A list of requirements that when satisfied, add or replace from the base starting gear. /// - [DataField("conditionalStartingGear")] + [DataField] public List? ConditionalStartingGears { get; private set; } /// @@ -184,7 +184,6 @@ namespace Content.Shared.Roles /// [DataField(required: true)] public ProtoId Id { get; private set; } - } /// diff --git a/Resources/Prototypes/Maps/Kettle.yml b/Resources/Prototypes/Maps/Kettle.yml index d545444a77..6114856017 100644 --- a/Resources/Prototypes/Maps/Kettle.yml +++ b/Resources/Prototypes/Maps/Kettle.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Silicons Borg: [ 2, 2 ] @@ -70,4 +68,4 @@ Musician: [ 1, 1 ] Reporter: [ 1, 1 ] Passenger: [ -1, -1 ] - Hobo: [ 1, 1 ] \ No newline at end of file + Hobo: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/Molecule.yml b/Resources/Prototypes/Maps/Molecule.yml index 3bd9d6e56c..849299b2ef 100644 --- a/Resources/Prototypes/Maps/Molecule.yml +++ b/Resources/Prototypes/Maps/Molecule.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Silicons StationAi: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/WhiteMeta.yml b/Resources/Prototypes/Maps/WhiteMeta.yml index dd59ccaf9c..af3636121c 100644 --- a/Resources/Prototypes/Maps/WhiteMeta.yml +++ b/Resources/Prototypes/Maps/WhiteMeta.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Silicons Borg: [ 2, 2 ] @@ -69,4 +67,4 @@ Musician: [ 1, 1 ] Reporter: [ 1, 1 ] Passenger: [ -1, -1 ] - Hobo: [ 1, 1 ] \ No newline at end of file + Hobo: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/Wonderbox.yml b/Resources/Prototypes/Maps/Wonderbox.yml index 149af09dba..45bb348e27 100644 --- a/Resources/Prototypes/Maps/Wonderbox.yml +++ b/Resources/Prototypes/Maps/Wonderbox.yml @@ -15,8 +15,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Silicons Borg: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/anchor.yml b/Resources/Prototypes/Maps/anchor.yml index 6bec366eb9..9cf8ec83bd 100644 --- a/Resources/Prototypes/Maps/anchor.yml +++ b/Resources/Prototypes/Maps/anchor.yml @@ -17,8 +17,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_UCLB.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #service Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/arena.yml b/Resources/Prototypes/Maps/arena.yml index 58aec6f55e..e403504080 100644 --- a/Resources/Prototypes/Maps/arena.yml +++ b/Resources/Prototypes/Maps/arena.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_UCLB.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #civilian Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/arenas.yml b/Resources/Prototypes/Maps/arenas.yml index 32f8543722..7ad7a16bc2 100644 --- a/Resources/Prototypes/Maps/arenas.yml +++ b/Resources/Prototypes/Maps/arenas.yml @@ -10,7 +10,5 @@ - type: StationNameSetup mapNameTemplate: "Meteor Arena" - type: StationJobs - overflowJobs: - - Passenger availableJobs: Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/asterisk.yml b/Resources/Prototypes/Maps/asterisk.yml index ba86f06809..8d7dbb857e 100644 --- a/Resources/Prototypes/Maps/asterisk.yml +++ b/Resources/Prototypes/Maps/asterisk.yml @@ -19,8 +19,6 @@ !type:NanotrasenNameGenerator prefixCreator: 'DV' - type: StationJobs - overflowJobs: - - Passenger availableJobs: #command Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/core.yml b/Resources/Prototypes/Maps/core.yml index 16f1f0c9f0..98341786ef 100644 --- a/Resources/Prototypes/Maps/core.yml +++ b/Resources/Prototypes/Maps/core.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_UCLB.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: # Service Bartender: [ 2, 3 ] diff --git a/Resources/Prototypes/Maps/debug.yml b/Resources/Prototypes/Maps/debug.yml index 133333ff8e..15b9f5127f 100644 --- a/Resources/Prototypes/Maps/debug.yml +++ b/Resources/Prototypes/Maps/debug.yml @@ -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 ] # Goobstation blob-config-start DEBUG @@ -50,7 +46,5 @@ - type: StationNameSetup mapNameTemplate: "TEG" - type: StationJobs - overflowJobs: - - ChiefEngineer availableJobs: ChiefEngineer: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/drydock.yml b/Resources/Prototypes/Maps/drydock.yml index 5ad5bf2a8d..b08acb4237 100644 --- a/Resources/Prototypes/Maps/drydock.yml +++ b/Resources/Prototypes/Maps/drydock.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Silicons Borg: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/edge.yml b/Resources/Prototypes/Maps/edge.yml index 68239c11c3..76af24753c 100644 --- a/Resources/Prototypes/Maps/edge.yml +++ b/Resources/Prototypes/Maps/edge.yml @@ -16,8 +16,6 @@ # !type:NanotrasenNameGenerator # prefixCreator: 'DV' # - type: StationJobs -# overflowJobs: -# - Passenger # availableJobs: # #service # Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/europa.yml b/Resources/Prototypes/Maps/europa.yml index b2b255a187..c1a559a9d8 100644 --- a/Resources/Prototypes/Maps/europa.yml +++ b/Resources/Prototypes/Maps/europa.yml @@ -14,8 +14,6 @@ # !type:NanotrasenNameGenerator # prefixCreator: 'B' # - type: StationJobs -# overflowJobs: -# - Passenger # availableJobs: # #service # Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/glacier.yml b/Resources/Prototypes/Maps/glacier.yml index 1880ba8156..e50e521a41 100644 --- a/Resources/Prototypes/Maps/glacier.yml +++ b/Resources/Prototypes/Maps/glacier.yml @@ -20,8 +20,6 @@ # mapPath: /Maps/Nonstations/glacier_surface.yml # biome: Snow - type: StationJobs - overflowJobs: - - Passenger availableJobs: Passenger: [ -1, -1 ] # Command diff --git a/Resources/Prototypes/Maps/hammurabi.yml b/Resources/Prototypes/Maps/hammurabi.yml index 8dd21aaef3..298f778d26 100644 --- a/Resources/Prototypes/Maps/hammurabi.yml +++ b/Resources/Prototypes/Maps/hammurabi.yml @@ -15,8 +15,6 @@ # - type: StationEmergencyShuttle # emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_Centipede.yml # - type: StationJobs -# overflowJobs: -# - Passenger # availableJobs: # #civilian # Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/hive.yml b/Resources/Prototypes/Maps/hive.yml index 68334f5399..70ba1b6e78 100644 --- a/Resources/Prototypes/Maps/hive.yml +++ b/Resources/Prototypes/Maps/hive.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_Seal.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #civilian Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/lighthouse.yml b/Resources/Prototypes/Maps/lighthouse.yml index 1b0a2d2f3c..aae8e8e736 100644 --- a/Resources/Prototypes/Maps/lighthouse.yml +++ b/Resources/Prototypes/Maps/lighthouse.yml @@ -16,8 +16,6 @@ !type:NanotrasenNameGenerator prefixCreator: '14' - type: StationJobs - overflowJobs: - - Passenger availableJobs: #command Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/moose.yml b/Resources/Prototypes/Maps/moose.yml index 5d7c9fd7fa..ca221c8580 100644 --- a/Resources/Prototypes/Maps/moose.yml +++ b/Resources/Prototypes/Maps/moose.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Silicons Borg: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/northway.yml b/Resources/Prototypes/Maps/northway.yml index 1a7fba66b5..64f55f3951 100644 --- a/Resources/Prototypes/Maps/northway.yml +++ b/Resources/Prototypes/Maps/northway.yml @@ -16,8 +16,6 @@ !type:NanotrasenNameGenerator prefixCreator: '14' - type: StationJobs - overflowJobs: - - Passenger availableJobs: #service Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/pebble.yml b/Resources/Prototypes/Maps/pebble.yml index 0e58ac55c8..c783d7bbbf 100644 --- a/Resources/Prototypes/Maps/pebble.yml +++ b/Resources/Prototypes/Maps/pebble.yml @@ -8,74 +8,72 @@ PebbleStation: stationProto: StandardNanotrasenStation components: - - type: StationNameSetup - mapNameTemplate: '{0} Pebble Station {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: 'NYA' -# - type: GridSpawn -# groups: -# AISAT: -# paths: -# - /Maps/Shuttles/AISAT.yml - - type: StationJobs - overflowJobs: - - Passenger - availableJobs: + - type: StationNameSetup + mapNameTemplate: '{0} Pebble Station {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: 'NYA' + # - type: GridSpawn + # groups: + # AISAT: + # paths: + # - /Maps/Shuttles/AISAT.yml + - type: StationJobs + availableJobs: #service - Captain: [ 1, 1 ] - # BlueshieldOfficer: [ 1, 1] - WD EDIT - # NanotrasenRepresentative: [ 1, 1 ] - WD EDIT - HeadOfPersonnel: [ 1, 1 ] - Bartender: [ 1, 2 ] - Botanist: [ 2, 2 ] - Chef: [ 2 , 2 ] - Clown: [ 1, 1 ] - Lawyer: [ 1, 1 ] - Musician: [ 1, 1 ] - Janitor: [ 1, 2 ] - Mime: [ 1, 1 ] + Captain: [ 1, 1 ] + # BlueshieldOfficer: [ 1, 1] - WD EDIT + # NanotrasenRepresentative: [ 1, 1 ] - WD EDIT + HeadOfPersonnel: [ 1, 1 ] + Bartender: [ 1, 2 ] + Botanist: [ 2, 2 ] + Chef: [ 2 , 2 ] + Clown: [ 1, 1 ] + Lawyer: [ 1, 1 ] + Musician: [ 1, 1 ] + Janitor: [ 1, 2 ] + Mime: [ 1, 1 ] #engineering - ChiefEngineer: [ 1, 1 ] - AtmosphericTechnician: [ 1, 1] - # SeniorEngineer: [ 1, 1 ] - WD EDIT - StationEngineer: [ 2, 3 ] - TechnicalAssistant: [ 2, 2 ] + ChiefEngineer: [ 1, 1 ] + AtmosphericTechnician: [ 1, 1] + # SeniorEngineer: [ 1, 1 ] - WD EDIT + StationEngineer: [ 2, 3 ] + TechnicalAssistant: [ 2, 2 ] #medical - ChiefMedicalOfficer: [ 1, 1 ] - Chemist: [ 1, 1 ] - # SeniorPhysician: [ 1, 1 ] - WD EDIT - MedicalDoctor: [ 2, 3 ] - MedicalIntern: [ 2, 2 ] - Paramedic: [ 1, 1 ] - Psychologist: [ 1, 1 ] + ChiefMedicalOfficer: [ 1, 1 ] + Chemist: [ 1, 1 ] + # SeniorPhysician: [ 1, 1 ] - WD EDIT + MedicalDoctor: [ 2, 3 ] + MedicalIntern: [ 2, 2 ] + Paramedic: [ 1, 1 ] + Psychologist: [ 1, 1 ] #science - ResearchDirector: [ 1, 1 ] - Chaplain: [ 1, 1 ] - ForensicMantis: [ 1, 1 ] - # SeniorResearcher: [ 1, 1] - WD EDIT - Scientist: [ 2, 3 ] - ResearchAssistant: [ 2, 2 ] - Borg: [ 1, 1 ] + ResearchDirector: [ 1, 1 ] + Chaplain: [ 1, 1 ] + ForensicMantis: [ 1, 1 ] + # SeniorResearcher: [ 1, 1] - WD EDIT + Scientist: [ 2, 3 ] + ResearchAssistant: [ 2, 2 ] + Borg: [ 1, 1 ] #security - HeadOfSecurity: [ 1, 1 ] - Warden: [ 1, 1 ] - Detective: [ 1, 1 ] - # SeniorOfficer: [ 1, 1 ] - WD EDIT - SecurityOfficer: [ 2, 2 ] - SecurityCadet: [ 1, 1 ] + HeadOfSecurity: [ 1, 1 ] + Warden: [ 1, 1 ] + Detective: [ 1, 1 ] + # SeniorOfficer: [ 1, 1 ] - WD EDIT + SecurityOfficer: [ 2, 2 ] + SecurityCadet: [ 1, 1 ] #supply - Quartermaster: [ 1, 1 ] - MailCarrier: [ 1, 2 ] - SalvageSpecialist: [ 2, 2 ] - CargoTechnician: [ 2, 3 ] + Quartermaster: [ 1, 1 ] + MailCarrier: [ 1, 2 ] + SalvageSpecialist: [ 2, 2 ] + CargoTechnician: [ 2, 3 ] #civilian - Passenger: [ -1, -1 ] + Passenger: [ -1, -1 ] # Silicon - StationAi: [ 1, 1 ] + StationAi: [ 1, 1 ] # blob-config-start SMALL+ - - type: StationBlobConfig - stageBegin: 25 - stageCritical: 350 - stageTheEnd: 700 + - type: StationBlobConfig + stageBegin: 25 + stageCritical: 350 + stageTheEnd: 700 # blob-config-end diff --git a/Resources/Prototypes/Maps/radstation.yml b/Resources/Prototypes/Maps/radstation.yml index c5ac815c7b..f68de18f1d 100644 --- a/Resources/Prototypes/Maps/radstation.yml +++ b/Resources/Prototypes/Maps/radstation.yml @@ -15,8 +15,6 @@ !type:NanotrasenNameGenerator prefixCreator: '14' - type: StationJobs - overflowJobs: - - Passenger availableJobs: #command Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/saltern.yml b/Resources/Prototypes/Maps/saltern.yml index e6f17c211f..d804ceff50 100644 --- a/Resources/Prototypes/Maps/saltern.yml +++ b/Resources/Prototypes/Maps/saltern.yml @@ -17,8 +17,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_Delta.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #command Captain: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/shoukou.yml b/Resources/Prototypes/Maps/shoukou.yml index 69999ab0fc..228ebc9174 100644 --- a/Resources/Prototypes/Maps/shoukou.yml +++ b/Resources/Prototypes/Maps/shoukou.yml @@ -16,8 +16,6 @@ - type: StationEmergencyShuttle emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_Delta.yml - type: StationJobs - overflowJobs: - - Passenger availableJobs: #Service Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/submarine.yml b/Resources/Prototypes/Maps/submarine.yml index a6a250c684..561bfc5bb2 100644 --- a/Resources/Prototypes/Maps/submarine.yml +++ b/Resources/Prototypes/Maps/submarine.yml @@ -15,8 +15,6 @@ # - type: StationEmergencyShuttle # emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_Propeller.yml # - type: StationJobs -# overflowJobs: -# - Passenger # availableJobs: # #civilian # Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/tortuga.yml b/Resources/Prototypes/Maps/tortuga.yml index 9ea75734fc..4e2f0588f2 100644 --- a/Resources/Prototypes/Maps/tortuga.yml +++ b/Resources/Prototypes/Maps/tortuga.yml @@ -15,8 +15,6 @@ # - type: StationEmergencyShuttle # emergencyShuttlePath: /Maps/Shuttles/DeltaV/NTES_Seal.yml # - type: StationJobs -# overflowJobs: -# - Passenger # availableJobs: # #civilian # Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Maps/whitebox.yml b/Resources/Prototypes/Maps/whitebox.yml index ac18eebf99..8675ddc0a8 100644 --- a/Resources/Prototypes/Maps/whitebox.yml +++ b/Resources/Prototypes/Maps/whitebox.yml @@ -15,8 +15,6 @@ # - type: StationEmergencyShuttle # emergencyShuttlePath: /Maps/_White/Shuttles/emergency_turtle.yml # - type: StationJobs -# overflowJobs: -# - Passenger # availableJobs: # #Silicons # Borg: [ 2, 2 ]