From ec68226e99a55524ad3c16efa6147884bd9505f3 Mon Sep 17 00:00:00 2001 From: Moony Date: Fri, 26 Nov 2021 03:02:46 -0600 Subject: [PATCH] Refactor how jobs are handed out (#5422) * Completely refactor how job spawning works * Remove remains of old system. * Squash the final bug, cleanup. * Attempt to fix tests * Adjusts packed's round-start crew roster, re-enables a bunch of old roles. Also adds the Central Command Official as a proper role. * pretty up ui * refactor StationSystem into the correct folder & namespace. * remove a log, make sure the lobby gets updated if a new map is spontaneously added. * re-add accidentally removed log * We do a little logging * we do a little resolving * we do a little documenting * Renamed OverflowJob to FallbackOverflowJob Allows stations to configure their own roundstart overflow job list. * narrator: it did not compile * oops * support having no overflow jobs * filescope for consistency * small fixes * Bumps a few role counts for Packed, namely engis * log moment * E * Update Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> * Update Content.Server/Maps/GameMapPrototype.cs Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> * factored job logic, cleanup. * e * Address reviews * Remove the concept of a "default" grid. It has no future in our new multi-station world * why was clickable using that in the first place * fix bad evil bug that almost slipped through also adds chemist * rms obsolete things from chemist * Adds a sanity fallback * address reviews * adds ability to set name * fuck * cleanup joingame --- .../GameTicking/Managers/ClientGameTicker.cs | 14 +- Content.Client/LateJoin/LateJoinGui.cs | 305 +++++++++++------- .../Lobby/UI/LobbyCharacterPreviewPanel.cs | 2 +- .../UI/HumanoidProfileEditor.xaml.cs | 2 +- .../Tests/ClickableTest.cs | 5 +- .../Commands/LoadGameMapCommand.cs | 66 ++++ .../GameTicking/Commands/JoinGameCommand.cs | 32 +- .../GameTicking/GameTicker.JobController.cs | 202 +++++------- .../GameTicking/GameTicker.Player.cs | 5 +- .../GameTicking/GameTicker.RoundFlow.cs | 41 ++- .../GameTicking/GameTicker.Spawning.cs | 68 +++- Content.Server/GameTicking/GameTicker.cs | 6 +- Content.Server/Maps/GameMapManager.cs | 205 ++++++------ Content.Server/Maps/GameMapPrototype.cs | 95 +++--- Content.Server/Maps/IGameMapManager.cs | 109 ++++--- Content.Server/Station/StationComponent.cs | 13 + Content.Server/Station/StationSystem.cs | 165 ++++++++++ .../StationEvents/Events/GasLeak.cs | 42 ++- .../StationEvents/Events/RadiationStorm.cs | 18 +- Content.Shared/Administration/Logs/LogType.cs | 3 + .../GameTicking/SharedGameTicker.cs | 13 +- .../Preferences/HumanoidCharacterProfile.cs | 4 +- Content.Shared/Roles/JobPrototype.cs | 14 - Content.Shared/Station/StationId.cs | 10 + .../Server/Preferences/ServerDbSqliteTests.cs | 2 +- .../Locale/en-US/late-join/late-join-gui.ftl | 4 +- .../Entities/Objects/Devices/pda.yml | 89 +++-- .../Objects/Misc/identification_cards.yml | 68 ++-- Resources/Prototypes/Maps/game.yml | 55 ++++ Resources/Prototypes/Maps/test.yml | 5 +- .../Roles/Jobs/Cargo/cargo_technician.yml | 2 - .../Roles/Jobs/Cargo/quartermaster.yml | 52 ++- .../Roles/Jobs/Civilian/assistant.yml | 1 - .../Roles/Jobs/Civilian/bartender.yml | 1 - .../Roles/Jobs/Civilian/botanist.yml | 2 - .../Roles/Jobs/Civilian/chaplain.yml | 41 ++- .../Prototypes/Roles/Jobs/Civilian/chef.yml | 1 - .../Prototypes/Roles/Jobs/Civilian/clown.yml | 1 - .../Roles/Jobs/Civilian/janitor.yml | 1 - .../Prototypes/Roles/Jobs/Civilian/mime.yml | 1 - .../Prototypes/Roles/Jobs/Command/captain.yml | 1 - .../Roles/Jobs/Command/centcom_official.yml | 12 + .../Roles/Jobs/Command/head_of_personnel.yml | 1 - .../Roles/Jobs/Engineering/chief_engineer.yml | 1 - .../Jobs/Engineering/station_engineer.yml | 2 - .../Prototypes/Roles/Jobs/Medical/chemist.yml | 2 - .../Jobs/Medical/chief_medical_officer.yml | 1 - .../Roles/Jobs/Medical/medical_doctor.yml | 2 - .../Roles/Jobs/Science/research_director.yml | 1 - .../Roles/Jobs/Science/scientist.yml | 2 - .../Roles/Jobs/Security/head_of_security.yml | 1 - .../Roles/Jobs/Security/security_officer.yml | 2 - .../Prototypes/Roles/Jobs/Security/warden.yml | 60 ++-- 53 files changed, 1148 insertions(+), 705 deletions(-) create mode 100644 Content.Server/Administration/Commands/LoadGameMapCommand.cs create mode 100644 Content.Server/Station/StationComponent.cs create mode 100644 Content.Server/Station/StationSystem.cs create mode 100644 Content.Shared/Station/StationId.cs diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index 888a4f736a..28a945a1cb 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.Client.Viewport; using Content.Shared.GameTicking; using Content.Shared.GameWindow; +using Content.Shared.Station; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.State; using Robust.Shared.IoC; +using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -21,7 +23,8 @@ namespace Content.Client.GameTicking.Managers [Dependency] private readonly IStateManager _stateManager = default!; [ViewVariables] private bool _initialized; - private readonly List _jobsAvailable = new(); + private Dictionary> _jobsAvailable = new(); + private Dictionary _stationNames = new(); [ViewVariables] public bool AreWeReady { get; private set; } [ViewVariables] public bool IsGameStarted { get; private set; } @@ -31,13 +34,14 @@ namespace Content.Client.GameTicking.Managers [ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public bool Paused { get; private set; } [ViewVariables] public Dictionary Status { get; private set; } = new(); - [ViewVariables] public IReadOnlyList JobsAvailable => _jobsAvailable; + [ViewVariables] public IReadOnlyDictionary> JobsAvailable => _jobsAvailable; + [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; public event Action? InfoBlobUpdated; public event Action? LobbyStatusUpdated; public event Action? LobbyReadyUpdated; public event Action? LobbyLateJoinStatusUpdated; - public event Action>? LobbyJobsAvailableUpdated; + public event Action>>? LobbyJobsAvailableUpdated; public override void Initialize() { @@ -69,8 +73,8 @@ namespace Content.Client.GameTicking.Managers private void UpdateJobsAvailable(TickerJobsAvailableEvent message) { - _jobsAvailable.Clear(); - _jobsAvailable.AddRange(message.JobsAvailable); + _jobsAvailable = message.JobsAvailableByStation; + _stationNames = message.StationNames; LobbyJobsAvailableUpdated?.Invoke(JobsAvailable); } diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index 77e6d0f09d..72fc9b198f 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -2,9 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; using Content.Client.GameTicking.Managers; +using Content.Client.HUD.UI; using Content.Shared.Roles; +using Content.Shared.Station; using Robust.Client.Console; -using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; @@ -25,10 +26,13 @@ namespace Content.Client.LateJoin [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!; - public event Action? SelectedId; + public event Action<(StationId, string)> SelectedId; - private readonly Dictionary _jobButtons = new(); - private readonly Dictionary _jobCategories = new(); + private readonly Dictionary> _jobButtons = new(); + private readonly Dictionary> _jobCategories = new(); + private readonly List _jobLists = new(); + + private readonly Control _base; public LateJoinGui() { @@ -38,136 +42,203 @@ namespace Content.Client.LateJoin var gameTicker = EntitySystem.Get(); Title = Loc.GetString("late-join-gui-title"); - - var jobList = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - var vBox = new BoxContainer + _base = new BoxContainer() { Orientation = LayoutOrientation.Vertical, - Children = - { - new ScrollContainer - { - VerticalExpand = true, - Children = - { - jobList - } - } - } + VerticalExpand = true, + Margin = new Thickness(0), }; - Contents.AddChild(vBox); + Contents.AddChild(_base); - var firstCategory = true; + RebuildUI(); - foreach (var job in _prototypeManager.EnumeratePrototypes().OrderBy(j => j.Name)) - { - foreach (var department in job.Departments) - { - if (!_jobCategories.TryGetValue(department, out var category)) - { - category = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Name = department, - ToolTip = Loc.GetString("late-join-gui-jobs-amount-in-department-tooltip", - ("departmentName", department)) - }; - - if (firstCategory) - { - firstCategory = false; - } - else - { - category.AddChild(new Control - { - MinSize = new Vector2(0, 23), - }); - } - - category.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")}, - Children = - { - new Label - { - Text = Loc.GetString("late-join-gui-department-jobs-label", ("departmentName", department)) - } - } - }); - - _jobCategories[department] = category; - jobList.AddChild(category); - } - - var jobButton = new JobButton(job.ID); - - var jobSelector = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true - }; - - var icon = new TextureRect - { - TextureScale = (2, 2), - Stretch = TextureRect.StretchMode.KeepCentered - }; - - if (job.Icon != null) - { - var specifier = new SpriteSpecifier.Rsi(new ResourcePath("/Textures/Interface/Misc/job_icons.rsi"), job.Icon); - icon.Texture = specifier.Frame0(); - } - - jobSelector.AddChild(icon); - - var jobLabel = new Label - { - Text = job.Name - }; - - jobSelector.AddChild(jobLabel); - jobButton.AddChild(jobSelector); - category.AddChild(jobButton); - - jobButton.OnPressed += _ => - { - SelectedId?.Invoke(jobButton.JobId); - }; - - if (!gameTicker.JobsAvailable.Contains(job.ID)) - { - jobButton.Disabled = true; - } - - _jobButtons[job.ID] = jobButton; - } - } - - SelectedId += jobId => + SelectedId += x => { + var (station, jobId) = x; Logger.InfoS("latejoin", $"Late joining as ID: {jobId}"); - _consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)}"); + _consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)} {station.Id}"); Close(); }; gameTicker.LobbyJobsAvailableUpdated += JobsAvailableUpdated; } - private void JobsAvailableUpdated(IReadOnlyList jobs) + private void RebuildUI() { - foreach (var (id, button) in _jobButtons) + _base.RemoveAllChildren(); + _jobLists.Clear(); + _jobButtons.Clear(); + _jobCategories.Clear(); + + var gameTicker = EntitySystem.Get(); + foreach (var (id, name) in gameTicker.StationNames) { - button.Disabled = !jobs.Contains(id); + var jobList = new BoxContainer + { + Orientation = LayoutOrientation.Vertical + }; + + var collapseButton = new ContainerButton() + { + HorizontalAlignment = HAlignment.Right, + ToggleMode = true, + Children = + { + new TextureRect + { + StyleClasses = { OptionButton.StyleClassOptionTriangle }, + Margin = new Thickness(8, 0), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + } + } + }; + + _base.AddChild(new StripeBack() + { + Children = { + new PanelContainer() + { + Children = + { + new Label() + { + StyleClasses = { "LabelBig" }, + Text = $"NTSS {name}", + Align = Label.AlignMode.Center, + }, + collapseButton + } + } + } + }); + var jobListScroll = new ScrollContainer() + { + VerticalExpand = true, + Children = {jobList}, + Visible = false, + }; + + if (_jobLists.Count == 0) + jobListScroll.Visible = true; + + _jobLists.Add(jobListScroll); + + _base.AddChild(jobListScroll); + + collapseButton.OnToggled += _ => + { + foreach (var section in _jobLists) + { + section.Visible = false; + } + jobListScroll.Visible = true; + }; + + var firstCategory = true; + + foreach (var job in gameTicker.JobsAvailable[id].OrderBy(x => x.Key)) + { + var prototype = _prototypeManager.Index(job.Key); + foreach (var department in prototype.Departments) + { + if (!_jobCategories.TryGetValue(id, out var _)) + _jobCategories[id] = new Dictionary(); + if (!_jobButtons.TryGetValue(id, out var _)) + _jobButtons[id] = new Dictionary(); + if (!_jobCategories[id].TryGetValue(department, out var category)) + { + category = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Name = department, + ToolTip = Loc.GetString("late-join-gui-jobs-amount-in-department-tooltip", + ("departmentName", department)) + }; + + if (firstCategory) + { + firstCategory = false; + } + else + { + category.AddChild(new Control + { + MinSize = new Vector2(0, 23), + }); + } + + category.AddChild(new PanelContainer + { + Children = + { + new Label + { + StyleClasses = { "LabelBig" }, + Text = Loc.GetString("late-join-gui-department-jobs-label", ("departmentName", department)) + } + } + }); + + _jobCategories[id][department] = category; + jobList.AddChild(category); + } + + var jobButton = new JobButton(prototype.ID, job.Value); + + var jobSelector = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true + }; + + var icon = new TextureRect + { + TextureScale = (2, 2), + Stretch = TextureRect.StretchMode.KeepCentered + }; + + if (prototype.Icon != null) + { + var specifier = new SpriteSpecifier.Rsi(new ResourcePath("/Textures/Interface/Misc/job_icons.rsi"), prototype.Icon); + icon.Texture = specifier.Frame0(); + } + + jobSelector.AddChild(icon); + + var jobLabel = new Label + { + Text = job.Value >= 0 ? + Loc.GetString("late-join-gui-job-slot-capped", ("jobName", prototype.Name), ("amount", job.Value)) : + Loc.GetString("late-join-gui-job-slot-uncapped", ("jobName", prototype.Name)) + }; + + jobSelector.AddChild(jobLabel); + jobButton.AddChild(jobSelector); + category.AddChild(jobButton); + + jobButton.OnPressed += _ => + { + SelectedId?.Invoke((id, jobButton.JobId)); + }; + + if (job.Value == 0) + { + jobButton.Disabled = true; + } + + _jobButtons[id][prototype.ID] = jobButton; + } + } } } + private void JobsAvailableUpdated(IReadOnlyDictionary> _) + { + RebuildUI(); + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -184,10 +255,12 @@ namespace Content.Client.LateJoin class JobButton : ContainerButton { public string JobId { get; } + public int Amount { get; } - public JobButton(string jobId) + public JobButton(string jobId, int amount) { JobId = jobId; + Amount = amount; AddStyleClass(StyleClassButton); } } diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs index 14a6a974bc..5bc657ae09 100644 --- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs +++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs @@ -144,7 +144,7 @@ namespace Content.Client.Lobby.UI var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key; - var job = protoMan.Index(highPriorityJob ?? SharedGameTicker.OverflowJob); + var job = protoMan.Index(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob); inventory.ClearAllSlotVisuals(); diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs index 4f55735b32..6b91912867 100644 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs @@ -307,7 +307,7 @@ namespace Content.Client.Preferences.UI (int) PreferenceUnavailableMode.StayInLobby); _preferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button", - ("overflowJob", Loc.GetString(SharedGameTicker.OverflowJobName))), + ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))), (int) PreferenceUnavailableMode.SpawnAsOverflow); _preferenceUnavailableButton.OnItemSelected += args => diff --git a/Content.IntegrationTests/Tests/ClickableTest.cs b/Content.IntegrationTests/Tests/ClickableTest.cs index 25e4399135..4d24722b49 100644 --- a/Content.IntegrationTests/Tests/ClickableTest.cs +++ b/Content.IntegrationTests/Tests/ClickableTest.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Content.Client.Clickable; using Content.Server.GameTicking; @@ -6,10 +7,8 @@ using NUnit.Framework; using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Timing; namespace Content.IntegrationTests.Tests { @@ -76,7 +75,7 @@ namespace Content.IntegrationTests.Tests await _server.WaitPost(() => { - var gridEnt = mapManager.GetGrid(gameTicker.DefaultGridId).GridEntityId; + var gridEnt = mapManager.GetAllGrids().First().GridEntityId; worldPos = serverEntManager.GetEntity(gridEnt).Transform.WorldPosition; var ent = serverEntManager.SpawnEntity(prototype, new EntityCoordinates(gridEnt, 0f, 0f)); diff --git a/Content.Server/Administration/Commands/LoadGameMapCommand.cs b/Content.Server/Administration/Commands/LoadGameMapCommand.cs new file mode 100644 index 0000000000..8b055cd390 --- /dev/null +++ b/Content.Server/Administration/Commands/LoadGameMapCommand.cs @@ -0,0 +1,66 @@ +using Content.Server.Maps; +using Content.Server.Roles; +using Content.Server.Station; +using Content.Shared.Administration; +using Robust.Server.Maps; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; + +namespace Content.Server.Administration.Commands +{ + [AdminCommand(AdminFlags.Fun)] + public sealed class LoadGameMapCommand : IConsoleCommand + { + public string Command => "loadgamemap"; + + public string Description => "Loads the given game map at the given coordinates."; + + public string Help => "loadgamemap [ []] "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var prototypeManager = IoCManager.Resolve(); + var mapLoader = IoCManager.Resolve(); + var entityManager = IoCManager.Resolve(); + var stationSystem = entityManager.EntitySysManager.GetEntitySystem(); + + if (args.Length is not (2 or 4 or 5)) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (prototypeManager.TryIndex(args[0], out var gameMap)) + { + if (int.TryParse(args[1], out var mapId)) + { + var gameMapEnt = mapLoader.LoadBlueprint(new MapId(mapId), gameMap.MapPath); + if (gameMapEnt is null) + { + shell.WriteError($"Failed to create the given game map, is the path {gameMap.MapPath} correct?"); + return; + } + + if (args.Length >= 4 && int.TryParse(args[2], out var x) && int.TryParse(args[3], out var y)) + { + var transform = entityManager.GetComponent(gameMapEnt.GridEntityId); + transform.WorldPosition = new Vector2(x, y); + } + + var stationName = args.Length == 5 ? args[4] : null; + + stationSystem.InitialSetupStationGrid(gameMapEnt.GridEntityId, gameMap, stationName); + } + } + else + { + shell.WriteError($"The given map prototype {args[0]} is invalid."); + } + } + } +} diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs index d0b060d59b..15d6a92027 100644 --- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs +++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs @@ -1,10 +1,14 @@ using System.Collections.Generic; using Content.Server.Administration; +using Content.Server.Roles; +using Content.Server.Station; using Content.Shared.Roles; +using Content.Shared.Station; using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Prototypes; namespace Content.Server.GameTicking.Commands @@ -24,35 +28,47 @@ namespace Content.Server.GameTicking.Commands } public void Execute(IConsoleShell shell, string argStr, string[] args) { + if (args.Length != 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + var player = shell.Player as IPlayerSession; - var output = string.Join(".", args); + if (player == null) { return; } var ticker = EntitySystem.Get(); + var stationSystem = EntitySystem.Get(); if (ticker.RunLevel == GameRunLevel.PreRoundLobby) { shell.WriteLine("Round has not started."); return; } - else if(ticker.RunLevel == GameRunLevel.InRound) + else if (ticker.RunLevel == GameRunLevel.InRound) { - string ID = args[0]; - var positions = ticker.GetAvailablePositions(); + string id = args[0]; - if(positions.GetValueOrDefault(ID, 0) == 0) //n < 0 is treated as infinite + if (!uint.TryParse(args[1], out var sid)) { - var jobPrototype = _prototypeManager.Index(ID); + shell.WriteError(Loc.GetString("shell-argument-must-be-number")); + } + + var stationId = new StationId(sid); + if(!stationSystem.IsJobAvailableOnStation(stationId, id)) + { + var jobPrototype = _prototypeManager.Index(id); shell.WriteLine($"{jobPrototype.Name} has no available slots."); return; } - ticker.MakeJoinGame(player, args[0]); + ticker.MakeJoinGame(player, stationId, id); return; } - ticker.MakeJoinGame(player); + ticker.MakeJoinGame(player, StationId.Invalid); } } } diff --git a/Content.Server/GameTicking/GameTicker.JobController.cs b/Content.Server/GameTicking/GameTicker.JobController.cs index a94bf6ab02..4916ac1668 100644 --- a/Content.Server/GameTicking/GameTicker.JobController.cs +++ b/Content.Server/GameTicking/GameTicker.JobController.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -6,6 +5,7 @@ using System.Linq; using Content.Shared.GameTicking; using Content.Shared.Preferences; using Content.Shared.Roles; +using Content.Shared.Station; using Robust.Server.Player; using Robust.Shared.Localization; using Robust.Shared.Network; @@ -25,131 +25,90 @@ namespace Content.Server.GameTicking [ViewVariables] private readonly Dictionary _spawnedPositions = new(); - private Dictionary AssignJobs(List available, + private Dictionary AssignJobs(List available, Dictionary profiles) { - // Calculate positions available round-start for each job. - var availablePositions = GetBasePositions(true); + var assigned = new Dictionary(); - // Output dictionary of assigned jobs. - var assigned = new Dictionary(); - - // Go over each priority level top to bottom. - for (var i = JobPriority.High; i > JobPriority.Never; i--) + List<(IPlayerSession, List)> GetPlayersJobCandidates(bool heads, JobPriority i) { - void ProcessJobs(bool heads) - { - // Get all candidates for this priority & heads combo. - // That is all people with at LEAST one job at this priority & heads level, - // and the jobs they have selected here. - var candidates = available - .Select(player => - { - var profile = profiles[player.UserId]; - - var availableJobs = profile.JobPriorities - .Where(j => - { - var (jobId, priority) = j; - if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job)) - { - // Job doesn't exist, probably old data? - return false; - } - if (job.IsHead != heads) - { - return false; - } - - return priority == i; - }) - .Select(j => j.Key) - .ToList(); - - return (player, availableJobs); - }) - .Where(p => p.availableJobs.Count != 0) - .ToList(); - - _robustRandom.Shuffle(candidates); - - foreach (var (candidate, jobs) in candidates) + return available.Select(player => { - while (jobs.Count != 0) - { - var picked = _robustRandom.Pick(jobs); + var profile = profiles[player.UserId]; - var openPositions = availablePositions.GetValueOrDefault(picked, 0); - if (openPositions == 0) + var availableJobs = profile.JobPriorities + .Where(j => { - jobs.Remove(picked); - continue; - } + var (jobId, priority) = j; + if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job)) + { + // Job doesn't exist, probably old data? + return false; + } - availablePositions[picked] -= 1; - assigned.Add(candidate, picked); - break; + if (job.IsHead != heads) + { + return false; + } + + return priority == i; + }) + .Select(j => j.Key) + .ToList(); + + return (player, availableJobs); + }) + .Where(p => p.availableJobs.Count != 0) + .ToList(); + } + + void ProcessJobs(bool heads, Dictionary availablePositions, StationId id, JobPriority i) + { + var candidates = GetPlayersJobCandidates(heads, i); + + foreach (var (candidate, jobs) in candidates) + { + while (jobs.Count != 0) + { + var picked = _robustRandom.Pick(jobs); + + var openPositions = availablePositions.GetValueOrDefault(picked, 0); + if (openPositions == 0) + { + jobs.Remove(picked); + continue; } - } - available.RemoveAll(a => assigned.ContainsKey(a)); + availablePositions[picked] -= 1; + assigned.Add(candidate, (picked, id)); + break; + } } - // Process heads FIRST. - // This means that if you have head and non-head roles on the same priority level, - // you will always get picked as head. - // Unless of course somebody beats you to those head roles. - ProcessJobs(true); - ProcessJobs(false); + available.RemoveAll(a => assigned.ContainsKey(a)); + } + + // Current strategy is to fill each station one by one. + foreach (var (id, station) in _stationSystem.StationInfo) + { + // Get the ROUND-START job list. + var availablePositions = station.MapPrototype.AvailableJobs.ToDictionary(x => x.Key, x => x.Value[0]); + + for (var i = JobPriority.High; i > JobPriority.Never; i--) + { + // Process jobs possible for heads... + ProcessJobs(true, availablePositions, id, i); + // and then jobs that are not heads. + ProcessJobs(false, availablePositions, id, i); + } } return assigned; } - /// - /// Gets the available positions for all jobs, *not* accounting for the current crew manifest. - /// - private Dictionary GetBasePositions(bool roundStart) + private string PickBestAvailableJob(HumanoidCharacterProfile profile, StationId station) { - var availablePositions = _prototypeManager - .EnumeratePrototypes() - // -1 is treated as infinite slots. - .ToDictionary(job => job.ID, job => - { - if (job.SpawnPositions < 0) - { - return int.MaxValue; - } - - if (roundStart) - { - return job.SpawnPositions; - } - - return job.TotalPositions; - }); - - return availablePositions; - } - - /// - /// Gets the remaining available job positions in the current round. - /// - public Dictionary GetAvailablePositions() - { - var basePositions = GetBasePositions(false); - - foreach (var (jobId, count) in _spawnedPositions) - { - basePositions[jobId] = Math.Max(0, basePositions[jobId] - count); - } - - return basePositions; - } - - private string PickBestAvailableJob(HumanoidCharacterProfile profile) - { - var available = GetAvailablePositions(); + var available = _stationSystem.StationInfo[station].JobList; bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId) { @@ -188,18 +147,17 @@ namespace Content.Server.GameTicking return picked; } - return OverflowJob; + var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone().ToList(); + return _robustRandom.Pick(overflows); } [Conditional("DEBUG")] private void InitializeJobController() { // Verify that the overflow role exists and has the correct name. - var role = _prototypeManager.Index(OverflowJob); - DebugTools.Assert(role.Name == Loc.GetString(OverflowJobName), + var role = _prototypeManager.Index(FallbackOverflowJob); + DebugTools.Assert(role.Name == Loc.GetString(FallbackOverflowJobName), "Overflow role does not have the correct name!"); - - DebugTools.Assert(role.SpawnPositions < 0, "Overflow role must have infinite spawn positions!"); } private void AddSpawnedPosition(string jobId) @@ -211,17 +169,21 @@ namespace Content.Server.GameTicking { // If late join is disallowed, return no available jobs. if (DisallowLateJoin) - return new TickerJobsAvailableEvent(Array.Empty()); + return new TickerJobsAvailableEvent(new Dictionary(), new Dictionary>()); - var jobs = GetAvailablePositions() - .Where(e => e.Value > 0) - .Select(e => e.Key) - .ToArray(); + var jobs = new Dictionary>(); + var stationNames = new Dictionary(); - return new TickerJobsAvailableEvent(jobs); + foreach (var (id, station) in _stationSystem.StationInfo) + { + var list = station.JobList.ToDictionary(x => x.Key, x => x.Value); + jobs.Add(id, list); + stationNames.Add(id, station.Name); + } + return new TickerJobsAvailableEvent(stationNames, jobs); } - private void UpdateJobsAvailable() + public void UpdateJobsAvailable() { RaiseNetworkEvent(GetJobsAvailable(), Filter.Empty().AddPlayers(_playersInLobby.Keys)); } diff --git a/Content.Server/GameTicking/GameTicker.Player.cs b/Content.Server/GameTicking/GameTicker.Player.cs index c8339338bd..bcf027b3b3 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -1,8 +1,11 @@ using System; using Content.Server.Players; +using Content.Server.Roles; +using Content.Server.Station; using Content.Shared.GameTicking; using Content.Shared.GameWindow; using Content.Shared.Preferences; +using Content.Shared.Station; using JetBrains.Annotations; using Robust.Server.Player; using Robust.Shared.Enums; @@ -107,7 +110,7 @@ namespace Content.Server.GameTicking async void SpawnWaitPrefs() { await _prefsManager.WaitPreferencesLoaded(session); - SpawnPlayer(session); + SpawnPlayer(session, StationId.Invalid); } async void AddPlayerToDb(Guid id) diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 074025c0f4..87d1f657c5 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -6,10 +6,13 @@ using Content.Server.GameTicking.Events; using Content.Server.Players; using Content.Server.Mind; using Content.Server.Ghost; +using Content.Server.Roles; +using Content.Server.Station; using Content.Shared.CCVar; using Content.Shared.Coordinates; using Content.Shared.GameTicking; using Content.Shared.Preferences; +using Content.Shared.Station; using Prometheus; using Robust.Server.Player; using Robust.Shared.GameObjects; @@ -64,14 +67,17 @@ namespace Content.Server.GameTicking { DefaultMap = _mapManager.CreateMap(); var startTime = _gameTiming.RealTime; - var map = _gameMapManager.GetSelectedMapChecked(true).MapPath; - var grid = _mapLoader.LoadBlueprint(DefaultMap, map); + var map = _gameMapManager.GetSelectedMapChecked(true); + var grid = _mapLoader.LoadBlueprint(DefaultMap, map.MapPath); + if (grid == null) { - throw new InvalidOperationException($"No grid found for map {map}"); + throw new InvalidOperationException($"No grid found for map {map.MapName}"); } + _stationSystem.InitialSetupStationGrid(grid.GridEntityId, map); + var stationXform = EntityManager.GetComponent(grid.GridEntityId); if (StationOffset) @@ -87,7 +93,6 @@ namespace Content.Server.GameTicking stationXform.LocalRotation = _robustRandom.NextFloat(MathF.Tau); } - DefaultGridId = grid.Index; _spawnPoint = grid.ToCoordinates(); var timeSpan = _gameTiming.RealTime - startTime; @@ -153,14 +158,36 @@ namespace Content.Server.GameTicking var profile = profiles[player.UserId]; if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow) { - assignedJobs.Add(player, OverflowJob); + // Pick a random station + var stations = _stationSystem.StationInfo.Keys.ToList(); + _robustRandom.Shuffle(stations); + + if (stations.Count == 0) + { + assignedJobs.Add(player, (FallbackOverflowJob, StationId.Invalid)); + continue; + } + + foreach (var station in stations) + { + // Pick a random overflow job from that station + var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone(); + _robustRandom.Shuffle(overflows); + + // Stations with no overflow slots should simply get skipped over. + if (overflows.Count == 0) + continue; + + // If the overflow exists, put them in as it. + assignedJobs.Add(player, (overflows[0], stations[0])); + } } } // Spawn everybody in! - foreach (var (player, job) in assignedJobs) + foreach (var (player, (job, station)) in assignedJobs) { - SpawnPlayer(player, profiles[player.UserId], job, false); + SpawnPlayer(player, profiles[player.UserId], station, job, false); } // Time to start the preset. diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index f85d20dd3c..b2cb7a9014 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Content.Server.Access.Components; using Content.Server.Access.Systems; -using Content.Server.CharacterAppearance.Components; using Content.Server.Ghost; using Content.Server.Ghost.Components; using Content.Server.Hands.Components; @@ -14,12 +14,15 @@ using Content.Server.Players; using Content.Server.Roles; using Content.Server.Spawners.Components; using Content.Server.Speech.Components; +using Content.Server.Station; +using Content.Shared.Administration.Logs; using Content.Shared.CharacterAppearance.Systems; using Content.Shared.GameTicking; using Content.Shared.Ghost; using Content.Shared.Inventory; using Content.Shared.Preferences; using Content.Shared.Roles; +using Content.Shared.Station; using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -28,6 +31,7 @@ using Robust.Shared.Map; using Robust.Shared.Random; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; +using static Content.Server.Station.StationSystem; namespace Content.Server.GameTicking { @@ -38,22 +42,35 @@ namespace Content.Server.GameTicking [Dependency] private readonly IdCardSystem _cardSystem = default!; + /// + /// Can't yet be removed because every test ever seems to depend on it. I'll make removing this a different PR. + /// [ViewVariables(VVAccess.ReadWrite)] private EntityCoordinates _spawnPoint; // Mainly to avoid allocations. private readonly List _possiblePositions = new(); - private void SpawnPlayer(IPlayerSession player, string? jobId = null, bool lateJoin = true) + private void SpawnPlayer(IPlayerSession player, StationId station, string? jobId = null, bool lateJoin = true) { var character = GetPlayerProfile(player); - SpawnPlayer(player, character, jobId, lateJoin); + SpawnPlayer(player, character, station, jobId, lateJoin); UpdateJobsAvailable(); } - private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, string? jobId = null, bool lateJoin = true) + private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, StationId station, string? jobId = null, bool lateJoin = true) { + if (station == StationId.Invalid) + { + var stations = _stationSystem.StationInfo.Keys.ToList(); + _robustRandom.Shuffle(stations); + if (stations.Count == 0) + station = StationId.Invalid; + else + station = stations[0]; + } + // Can't spawn players with a dummy ticker! if (DummyTicker) return; @@ -78,7 +95,7 @@ namespace Content.Server.GameTicking newMind.ChangeOwningPlayer(data.UserId); // Pick best job best on prefs. - jobId ??= PickBestAvailableJob(character); + jobId ??= PickBestAvailableJob(character, station); var jobPrototype = _prototypeManager.Index(jobId); var job = new Job(newMind, jobPrototype); @@ -94,7 +111,7 @@ namespace Content.Server.GameTicking playDefaultSound: false); } - var mob = SpawnPlayerMob(job, character, lateJoin); + var mob = SpawnPlayerMob(job, character, station, lateJoin); newMind.TransferTo(mob.Uid); if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}")) @@ -111,20 +128,28 @@ namespace Content.Server.GameTicking jobSpecial.AfterEquip(mob); } + _stationSystem.TryAssignJobToStation(station, jobId); + + if (lateJoin) + _adminLogSystem.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {mob} as a {job.Name:jobName}."); + else + _adminLogSystem.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {mob} as a {job.Name:jobName}."); + Preset?.OnSpawnPlayerCompleted(player, mob, lateJoin); } public void Respawn(IPlayerSession player) { player.ContentData()?.WipeMind(); + _adminLogSystem.Add(LogType.Respawn, LogImpact.Medium, $"Player {player} was respawned."); if (LobbyEnabled) PlayerJoinLobby(player); else - SpawnPlayer(player); + SpawnPlayer(player, StationId.Invalid); } - public void MakeJoinGame(IPlayerSession player, string? jobId = null) + public void MakeJoinGame(IPlayerSession player, StationId station, string? jobId = null) { if (!_playersInLobby.ContainsKey(player)) return; @@ -133,7 +158,7 @@ namespace Content.Server.GameTicking return; } - SpawnPlayer(player, jobId); + SpawnPlayer(player, station, jobId); } public void MakeObserve(IPlayerSession player) @@ -168,9 +193,9 @@ namespace Content.Server.GameTicking } #region Mob Spawning Helpers - private IEntity SpawnPlayerMob(Job job, HumanoidCharacterProfile? profile, bool lateJoin = true) + private IEntity SpawnPlayerMob(Job job, HumanoidCharacterProfile? profile, StationId station, bool lateJoin = true) { - var coordinates = lateJoin ? GetLateJoinSpawnPoint() : GetJobSpawnPoint(job.Prototype.ID); + var coordinates = lateJoin ? GetLateJoinSpawnPoint(station) : GetJobSpawnPoint(job.Prototype.ID, station); var entity = EntityManager.SpawnEntity(PlayerPrototypeName, coordinates); if (job.StartingGear != null) @@ -255,7 +280,7 @@ namespace Content.Server.GameTicking } #region Spawn Points - public EntityCoordinates GetJobSpawnPoint(string jobId) + public EntityCoordinates GetJobSpawnPoint(string jobId, StationId station) { var location = _spawnPoint; @@ -263,17 +288,24 @@ namespace Content.Server.GameTicking foreach (var (point, transform) in EntityManager.EntityQuery()) { - if (point.SpawnType == SpawnPointType.Job && point.Job?.ID == jobId) + var matchingStation = + EntityManager.TryGetComponent(transform.ParentUid, out var stationComponent) && + stationComponent.Station == station; + DebugTools.Assert(EntityManager.TryGetComponent(transform.ParentUid, out _)); + + if (point.SpawnType == SpawnPointType.Job && point.Job?.ID == jobId && matchingStation) _possiblePositions.Add(transform.Coordinates); } if (_possiblePositions.Count != 0) location = _robustRandom.Pick(_possiblePositions); + else + location = GetLateJoinSpawnPoint(station); // We need a sane fallback here, so latejoin it is. return location; } - public EntityCoordinates GetLateJoinSpawnPoint() + public EntityCoordinates GetLateJoinSpawnPoint(StationId station) { var location = _spawnPoint; @@ -281,7 +313,13 @@ namespace Content.Server.GameTicking foreach (var (point, transform) in EntityManager.EntityQuery()) { - if (point.SpawnType == SpawnPointType.LateJoin) _possiblePositions.Add(transform.Coordinates); + var matchingStation = + EntityManager.TryGetComponent(transform.ParentUid, out var stationComponent) && + stationComponent.Station == station; + DebugTools.Assert(EntityManager.TryGetComponent(transform.ParentUid, out _)); + + if (point.SpawnType == SpawnPointType.LateJoin && matchingStation) + _possiblePositions.Add(transform.Coordinates); } if (_possiblePositions.Count != 0) diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 20cff11e69..91c0d74492 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -1,6 +1,9 @@ +using Content.Server.Administration.Logs; using Content.Server.Chat.Managers; using Content.Server.Maps; using Content.Server.Preferences.Managers; +using Content.Server.Roles; +using Content.Server.Station; using Content.Shared.Chat; using Content.Shared.GameTicking; using Content.Shared.GameWindow; @@ -27,7 +30,6 @@ namespace Content.Server.GameTicking [ViewVariables] private bool _postInitialized; [ViewVariables] public MapId DefaultMap { get; private set; } - [ViewVariables] public GridId DefaultGridId { get; private set; } public override void Initialize() { @@ -87,5 +89,7 @@ namespace Content.Server.GameTicking [Dependency] private readonly IWatchdogApi _watchdogApi = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IGameMapManager _gameMapManager = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly AdminLogSystem _adminLogSystem = default!; } } diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index ac6e151305..01a599d46d 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -11,110 +11,109 @@ using Robust.Shared.Localization; using Robust.Shared.Prototypes; using Robust.Shared.Random; -namespace Content.Server.Maps +namespace Content.Server.Maps; + +public class GameMapManager : IGameMapManager { - public class GameMapManager : IGameMapManager + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + + private GameMapPrototype _currentMap = default!; + private bool _currentMapForced; + + public void Initialize() { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IConfigurationManager _configurationManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - - private GameMapPrototype _currentMap = default!; - private bool _currentMapForced; - - public void Initialize() + _configurationManager.OnValueChanged(CCVars.GameMap, value => { - _configurationManager.OnValueChanged(CCVars.GameMap, value => - { - if (TryLookupMap(value, out var map)) - _currentMap = map; - else - throw new ArgumentException($"Unknown map prototype {value} was selected!"); - }, true); - _configurationManager.OnValueChanged(CCVars.GameMapForced, value => _currentMapForced = value, true); - } - - public IEnumerable CurrentlyEligibleMaps() - { - var maps = AllVotableMaps().Where(IsMapEligible).ToArray(); - - return maps.Length == 0 ? AllMaps().Where(x => x.Fallback) : maps; - } - - public IEnumerable AllVotableMaps() - { - return _prototypeManager.EnumeratePrototypes().Where(x => x.Votable); - } - - public IEnumerable AllMaps() - { - return _prototypeManager.EnumeratePrototypes(); - } - - public bool TrySelectMap(string gameMap) - { - if (!TryLookupMap(gameMap, out var map) || !IsMapEligible(map)) return false; - - _currentMap = map; - _currentMapForced = false; - return true; - - } - - public void ForceSelectMap(string gameMap) - { - if (!TryLookupMap(gameMap, out var map)) - throw new ArgumentException($"The map \"{gameMap}\" is invalid!"); - _currentMap = map; - _currentMapForced = true; - } - - public void SelectRandomMap() - { - var maps = CurrentlyEligibleMaps().ToList(); - _random.Shuffle(maps); - _currentMap = maps[0]; - _currentMapForced = false; - } - - public GameMapPrototype GetSelectedMap() - { - return _currentMap; - } - - public GameMapPrototype GetSelectedMapChecked(bool loud = false) - { - if (!_currentMapForced && !IsMapEligible(GetSelectedMap())) - { - var oldMap = GetSelectedMap().MapName; - SelectRandomMap(); - if (loud) - { - _chatManager.DispatchServerAnnouncement( - Loc.GetString("gamemap-could-not-use-map-error", - ("oldMap", oldMap), ("newMap", GetSelectedMap().MapName) - )); - } - } - - return GetSelectedMap(); - } - - public bool CheckMapExists(string gameMap) - { - return TryLookupMap(gameMap, out _); - } - - private bool IsMapEligible(GameMapPrototype map) - { - return map.MaxPlayers >= _playerManager.PlayerCount && map.MinPlayers <= _playerManager.PlayerCount; - } - - private bool TryLookupMap(string gameMap, [NotNullWhen(true)] out GameMapPrototype? map) - { - return _prototypeManager.TryIndex(gameMap, out map); - } + if (TryLookupMap(value, out var map)) + _currentMap = map; + else + throw new ArgumentException($"Unknown map prototype {value} was selected!"); + }, true); + _configurationManager.OnValueChanged(CCVars.GameMapForced, value => _currentMapForced = value, true); } -} + + public IEnumerable CurrentlyEligibleMaps() + { + var maps = AllVotableMaps().Where(IsMapEligible).ToArray(); + + return maps.Length == 0 ? AllMaps().Where(x => x.Fallback) : maps; + } + + public IEnumerable AllVotableMaps() + { + return _prototypeManager.EnumeratePrototypes().Where(x => x.Votable); + } + + public IEnumerable AllMaps() + { + return _prototypeManager.EnumeratePrototypes(); + } + + public bool TrySelectMap(string gameMap) + { + if (!TryLookupMap(gameMap, out var map) || !IsMapEligible(map)) return false; + + _currentMap = map; + _currentMapForced = false; + return true; + + } + + public void ForceSelectMap(string gameMap) + { + if (!TryLookupMap(gameMap, out var map)) + throw new ArgumentException($"The map \"{gameMap}\" is invalid!"); + _currentMap = map; + _currentMapForced = true; + } + + public void SelectRandomMap() + { + var maps = CurrentlyEligibleMaps().ToList(); + _random.Shuffle(maps); + _currentMap = maps[0]; + _currentMapForced = false; + } + + public GameMapPrototype GetSelectedMap() + { + return _currentMap; + } + + public GameMapPrototype GetSelectedMapChecked(bool loud = false) + { + if (!_currentMapForced && !IsMapEligible(GetSelectedMap())) + { + var oldMap = GetSelectedMap().MapName; + SelectRandomMap(); + if (loud) + { + _chatManager.DispatchServerAnnouncement( + Loc.GetString("gamemap-could-not-use-map-error", + ("oldMap", oldMap), ("newMap", GetSelectedMap().MapName) + )); + } + } + + return GetSelectedMap(); + } + + public bool CheckMapExists(string gameMap) + { + return TryLookupMap(gameMap, out _); + } + + private bool IsMapEligible(GameMapPrototype map) + { + return map.MaxPlayers >= _playerManager.PlayerCount && map.MinPlayers <= _playerManager.PlayerCount; + } + + private bool TryLookupMap(string gameMap, [NotNullWhen(true)] out GameMapPrototype? map) + { + return _prototypeManager.TryIndex(gameMap, out map); + } +} \ No newline at end of file diff --git a/Content.Server/Maps/GameMapPrototype.cs b/Content.Server/Maps/GameMapPrototype.cs index 0ee82bb08e..2305a69d8e 100644 --- a/Content.Server/Maps/GameMapPrototype.cs +++ b/Content.Server/Maps/GameMapPrototype.cs @@ -1,53 +1,70 @@ +using System.Collections.Generic; +using Content.Shared.Roles; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; using Robust.Shared.ViewVariables; -namespace Content.Server.Maps +namespace Content.Server.Maps; + +/// +/// Prototype data for a game map. +/// +[Prototype("gameMap")] +public class GameMapPrototype : IPrototype { + /// + [DataField("id", required: true)] + public string ID { get; } = default!; + /// - /// Prototype data for a game map. + /// Minimum players for the given map. /// - [Prototype("gameMap")] - public class GameMapPrototype : IPrototype - { - /// - [ViewVariables, DataField("id", required: true)] - public string ID { get; } = default!; + [DataField("minPlayers", required: true)] + public uint MinPlayers { get; } - /// - /// Minimum players for the given map. - /// - [ViewVariables, DataField("minPlayers", required: true)] - public uint MinPlayers { get; } + /// + /// Maximum players for the given map. + /// + [DataField("maxPlayers")] + public uint MaxPlayers { get; } = uint.MaxValue; - /// - /// Maximum players for the given map. - /// - [ViewVariables, DataField("maxPlayers")] - public uint MaxPlayers { get; } = uint.MaxValue; + /// + /// Name of the given map. + /// + [DataField("mapName", required: true)] + public string MapName { get; } = default!; - /// - /// Name of the given map. - /// - [ViewVariables, DataField("mapName", required: true)] - public string MapName { get; } = default!; + /// + /// Relative directory path to the given map, i.e. `Maps/saltern.yml` + /// + [DataField("mapPath", required: true)] + public string MapPath { get; } = default!; - /// - /// Relative directory path to the given map, i.e. `Maps/saltern.yml` - /// - [ViewVariables, DataField("mapPath", required: true)] - public string MapPath { get; } = default!; + /// + /// Controls if the map can be used as a fallback if no maps are eligible. + /// + [DataField("fallback")] + public bool Fallback { get; } - /// - /// Controls if the map can be used as a fallback if no maps are eligible. - /// - [ViewVariables, DataField("fallback")] - public bool Fallback { get; } + /// + /// Controls if the map can be voted for. + /// + [DataField("votable")] + public bool Votable { get; } = true; - /// - /// Controls if the map can be voted for. - /// - [ViewVariables, DataField("votable")] - public bool Votable { get; } = true; - } + /// + /// Jobs used at round start should the station run out of job slots. + /// Doesn't necessarily mean the station has infinite slots for the given jobs midround! + /// + [DataField("overflowJobs", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer))] + public List OverflowJobs { get; } = default!; + + /// + /// Index of all jobs available on the station, of form + /// jobname: [roundstart, midround] + /// + [DataField("availableJobs", required: true, customTypeSerializer:typeof(PrototypeIdDictionarySerializer, JobPrototype>))] + public Dictionary> AvailableJobs { get; } = default!; } diff --git a/Content.Server/Maps/IGameMapManager.cs b/Content.Server/Maps/IGameMapManager.cs index 5af6eadfd4..2fe6eefde0 100644 --- a/Content.Server/Maps/IGameMapManager.cs +++ b/Content.Server/Maps/IGameMapManager.cs @@ -1,68 +1,67 @@ using System.Collections.Generic; -namespace Content.Server.Maps +namespace Content.Server.Maps; + +/// +/// Manages which station map will be used for the next round. +/// +public interface IGameMapManager { + void Initialize(); + /// - /// Manages which station map will be used for the next round. + /// Returns all maps eligible to be played right now. /// - public interface IGameMapManager - { - void Initialize(); + /// enumerator of map prototypes + IEnumerable CurrentlyEligibleMaps(); - /// - /// Returns all maps eligible to be played right now. - /// - /// enumerator of map prototypes - IEnumerable CurrentlyEligibleMaps(); + /// + /// Returns all maps that can be voted for. + /// + /// enumerator of map prototypes + IEnumerable AllVotableMaps(); - /// - /// Returns all maps that can be voted for. - /// - /// enumerator of map prototypes - IEnumerable AllVotableMaps(); + /// + /// Returns all maps. + /// + /// enumerator of map prototypes + IEnumerable AllMaps(); - /// - /// Returns all maps. - /// - /// enumerator of map prototypes - IEnumerable AllMaps(); + /// + /// Attempts to select the given map. + /// + /// map prototype + /// success or failure + bool TrySelectMap(string gameMap); - /// - /// Attempts to select the given map. - /// - /// map prototype - /// success or failure - bool TrySelectMap(string gameMap); + /// + /// Forces the given map, making sure the game map manager won't reselect if conditions are no longer met at round restart. + /// + /// map prototype + /// success or failure + void ForceSelectMap(string gameMap); - /// - /// Forces the given map, making sure the game map manager won't reselect if conditions are no longer met at round restart. - /// - /// map prototype - /// success or failure - void ForceSelectMap(string gameMap); + /// + /// Selects a random map. + /// + void SelectRandomMap(); - /// - /// Selects a random map. - /// - void SelectRandomMap(); + /// + /// Gets the currently selected map, without double-checking if it can be used. + /// + /// selected map + GameMapPrototype GetSelectedMap(); - /// - /// Gets the currently selected map, without double-checking if it can be used. - /// - /// selected map - GameMapPrototype GetSelectedMap(); + /// + /// Gets the currently selected map, double-checking if it can be used. + /// + /// selected map + GameMapPrototype GetSelectedMapChecked(bool loud = false); - /// - /// Gets the currently selected map, double-checking if it can be used. - /// - /// selected map - GameMapPrototype GetSelectedMapChecked(bool loud = false); - - /// - /// Checks if the given map exists - /// - /// name of the map - /// existence - bool CheckMapExists(string gameMap); - } -} + /// + /// Checks if the given map exists + /// + /// name of the map + /// existence + bool CheckMapExists(string gameMap); +} \ No newline at end of file diff --git a/Content.Server/Station/StationComponent.cs b/Content.Server/Station/StationComponent.cs new file mode 100644 index 0000000000..94e2c36338 --- /dev/null +++ b/Content.Server/Station/StationComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Station; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; + +namespace Content.Server.Station; + +[RegisterComponent, Friend(typeof(StationSystem))] +public class StationComponent : Component +{ + public override string Name => "Station"; + + public StationId Station = StationId.Invalid; +} diff --git a/Content.Server/Station/StationSystem.cs b/Content.Server/Station/StationSystem.cs new file mode 100644 index 0000000000..782ef04a74 --- /dev/null +++ b/Content.Server/Station/StationSystem.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Server.GameTicking; +using Content.Server.Maps; +using Content.Shared.Station; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; + +namespace Content.Server.Station; + +/// +/// System that manages the jobs available on a station, and maybe other things later. +/// +public class StationSystem : EntitySystem +{ + [Dependency] private GameTicker _gameTicker = default!; + private uint _idCounter = 1; + + private Dictionary _stationInfo = new(); + /// + /// List of stations for the current round. + /// + public IReadOnlyDictionary StationInfo => _stationInfo; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRoundEnd); + } + + /// + /// Cleans up station info. + /// + private void OnRoundEnd(GameRunLevelChangedEvent eventArgs) + { + if (eventArgs.New == GameRunLevel.PostRound) + _stationInfo = new(); + } + + public class StationInfoData + { + public readonly string Name; + + /// + /// Job list associated with the game map. + /// + public readonly GameMapPrototype MapPrototype; + + /// + /// The round job list. + /// + private readonly Dictionary _jobList; + + public IReadOnlyDictionary JobList => _jobList; + + public StationInfoData(string name, GameMapPrototype mapPrototype, Dictionary jobList) + { + Name = name; + MapPrototype = mapPrototype; + _jobList = jobList; + } + + public bool TryAssignJob(string jobName) + { + if (_jobList.ContainsKey(jobName)) + { + switch (_jobList[jobName]) + { + case > 0: + _jobList[jobName]--; + return true; + case -1: + return true; + default: + return false; + } + } + else + { + return false; + } + } + } + + /// + /// Creates a new station and attaches it to the given grid. + /// + /// grid to attach to + /// game map prototype of the station + /// name of the station to assign, if not the default + /// optional grid component of the grid. + /// The ID of the resulting station + /// Thrown when the given entity is not a grid. + public StationId InitialSetupStationGrid(EntityUid mapGrid, GameMapPrototype mapPrototype, string? stationName = null, IMapGridComponent? gridComponent = null) + { + if (!Resolve(mapGrid, ref gridComponent)) + throw new ArgumentException("Tried to initialize a station on a non-grid entity!"); + + var jobListDict = mapPrototype.AvailableJobs.ToDictionary(x => x.Key, x => x.Value[1]); + var id = AllocateStationInfo(); + + _stationInfo[id] = new StationInfoData(stationName ?? mapPrototype.MapName, mapPrototype, jobListDict); + var station = EntityManager.AddComponent(mapGrid); + station.Station = id; + + _gameTicker.UpdateJobsAvailable(); // new station means new jobs, tell any lobby-goers. + + Logger.InfoS("stations", + $"Setting up new {mapPrototype.ID} called {mapPrototype.MapName} on grid {mapGrid}:{gridComponent.GridIndex}"); + + return id; + } + + /// + /// Adds the given grid to the given station. + /// + /// grid to attach + /// station to attach the grid to + /// optional grid component of the grid. + /// Thrown when the given entity is not a grid. + public void AddGridToStation(EntityUid mapGrid, StationId station, IMapGridComponent? gridComponent = null) + { + if (!Resolve(mapGrid, ref gridComponent)) + throw new ArgumentException("Tried to initialize a station on a non-grid entity!"); + var stationComponent = EntityManager.AddComponent(mapGrid); + stationComponent.Station = station; + + Logger.InfoS("stations", $"Adding grid {mapGrid}:{gridComponent.GridIndex} to station {station} named {_stationInfo[station].Name}"); + } + + /// + /// Attempts to assign a job on the given station. + /// + /// station to assign to + /// name of the job + /// assignment success + public bool TryAssignJobToStation(StationId stationId, string jobName) + { + if (stationId != StationId.Invalid) + return _stationInfo[stationId].TryAssignJob(jobName); + else + return false; + } + + /// + /// Checks if the given job is available. + /// + /// station to check + /// name of the job + /// job availability + public bool IsJobAvailableOnStation(StationId stationId, string jobName) + { + if (_stationInfo[stationId].JobList.TryGetValue(jobName, out var amount)) + return amount != 0; + + return false; + } + + private StationId AllocateStationInfo() + { + return new StationId(_idCounter++); + } +} diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs index b1108edcae..8f5313b12f 100644 --- a/Content.Server/StationEvents/Events/GasLeak.cs +++ b/Content.Server/StationEvents/Events/GasLeak.cs @@ -1,7 +1,8 @@ -using Content.Server.Atmos.Components; +using System.Linq; using Content.Server.Atmos.EntitySystems; -using Content.Server.GameTicking; +using Content.Server.Station; using Content.Shared.Atmos; +using Content.Shared.Station; using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -15,6 +16,9 @@ namespace Content.Server.StationEvents.Events { internal sealed class GasLeak : StationEvent { + [Dependency] private IRobustRandom _robustRandom = default!; + [Dependency] private IEntityManager _entityManager = default!; + public override string Name => "GasLeak"; public override string? StartAnnouncement => @@ -56,6 +60,8 @@ namespace Content.Server.StationEvents.Events // Event variables + private StationId _targetStation; + private IEntity? _targetGrid; private Vector2i _targetTile; @@ -84,17 +90,16 @@ namespace Content.Server.StationEvents.Events public override void Startup() { base.Startup(); - var robustRandom = IoCManager.Resolve(); // Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there. - if (TryFindRandomTile(out _targetTile, robustRandom)) + if (TryFindRandomTile(out _targetTile)) { _foundTile = true; - _leakGas = robustRandom.Pick(LeakableGases); + _leakGas = _robustRandom.Pick(LeakableGases); // Was 50-50 on using normal distribution. - var totalGas = (float) robustRandom.Next(MinimumGas, MaximumGas); - _molesPerSecond = robustRandom.Next(MinimumMolesPerSecond, MaximumMolesPerSecond); + var totalGas = (float) _robustRandom.Next(MinimumGas, MaximumGas); + _molesPerSecond = _robustRandom.Next(MinimumMolesPerSecond, MaximumMolesPerSecond); EndAfter = totalGas / _molesPerSecond + StartAfter; Logger.InfoS("stationevents", $"Leaking {totalGas} of {_leakGas} over {EndAfter - StartAfter} seconds at {_targetTile}"); } @@ -147,8 +152,7 @@ namespace Content.Server.StationEvents.Events private void Spark() { var atmosphereSystem = EntitySystem.Get(); - var robustRandom = IoCManager.Resolve(); - if (robustRandom.NextFloat() <= SparkChance) + if (_robustRandom.NextFloat() <= SparkChance) { if (!_foundTile || _targetGrid == null || @@ -165,27 +169,31 @@ namespace Content.Server.StationEvents.Events } } - private bool TryFindRandomTile(out Vector2i tile, IRobustRandom? robustRandom = null) + private bool TryFindRandomTile(out Vector2i tile) { tile = default; - var defaultGridId = EntitySystem.Get().DefaultGridId; - if (!IoCManager.Resolve().TryGetGrid(defaultGridId, out var grid) || - !IoCManager.Resolve().TryGetEntity(grid.GridEntityId, out _targetGrid)) return false; + _targetStation = _robustRandom.Pick(_entityManager.EntityQuery().ToArray()).Station; + var possibleTargets = _entityManager.EntityQuery() + .Where(x => x.Station == _targetStation).ToArray(); + _targetGrid = _robustRandom.Pick(possibleTargets).Owner; + + if (!_entityManager.TryGetComponent(_targetGrid!.Uid, out var gridComp)) + return false; + var grid = gridComp.Grid; var atmosphereSystem = EntitySystem.Get(); - robustRandom ??= IoCManager.Resolve(); var found = false; var gridBounds = grid.WorldBounds; var gridPos = grid.WorldPosition; for (var i = 0; i < 10; i++) { - var randomX = robustRandom.Next((int) gridBounds.Left, (int) gridBounds.Right); - var randomY = robustRandom.Next((int) gridBounds.Bottom, (int) gridBounds.Top); + var randomX = _robustRandom.Next((int) gridBounds.Left, (int) gridBounds.Right); + var randomY = _robustRandom.Next((int) gridBounds.Bottom, (int) gridBounds.Top); tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y); - if (atmosphereSystem.IsTileSpace(defaultGridId, tile) || atmosphereSystem.IsTileAirBlocked(defaultGridId, tile)) continue; + if (atmosphereSystem.IsTileSpace(grid, tile) || atmosphereSystem.IsTileAirBlocked(grid, tile)) continue; found = true; _targetCoords = grid.GridTileToLocal(tile); break; diff --git a/Content.Server/StationEvents/Events/RadiationStorm.cs b/Content.Server/StationEvents/Events/RadiationStorm.cs index d0ded190d6..3b37e44584 100644 --- a/Content.Server/StationEvents/Events/RadiationStorm.cs +++ b/Content.Server/StationEvents/Events/RadiationStorm.cs @@ -1,6 +1,8 @@ -using Content.Server.GameTicking; +using System.Linq; using Content.Server.Radiation; +using Content.Server.Station; using Content.Shared.Coordinates; +using Content.Shared.Station; using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -29,6 +31,7 @@ namespace Content.Server.StationEvents.Events private float _timeUntilPulse; private const float MinPulseDelay = 0.2f; private const float MaxPulseDelay = 0.8f; + private StationId _target = StationId.Invalid; private void ResetTimeUntilPulse() { @@ -44,6 +47,7 @@ namespace Content.Server.StationEvents.Events public override void Startup() { ResetTimeUntilPulse(); + _target = _robustRandom.Pick(_entityManager.EntityQuery().ToArray()).Station; base.Startup(); } @@ -63,12 +67,18 @@ namespace Content.Server.StationEvents.Events if (_timeUntilPulse <= 0.0f) { var pauseManager = IoCManager.Resolve(); - var defaultGrid = IoCManager.Resolve().GetGrid(EntitySystem.Get().DefaultGridId); + // Account for split stations by just randomly picking a piece of it. + var possibleTargets = _entityManager.EntityQuery() + .Where(x => x.Station == _target).ToArray(); + var stationEnt = _robustRandom.Pick(possibleTargets).OwnerUid; - if (pauseManager.IsGridPaused(defaultGrid)) + if (!_entityManager.TryGetComponent(stationEnt, out var grid)) return; - SpawnPulse(defaultGrid); + if (pauseManager.IsGridPaused(grid.GridIndex)) + return; + + SpawnPulse(grid.Grid); } } diff --git a/Content.Shared/Administration/Logs/LogType.cs b/Content.Shared/Administration/Logs/LogType.cs index c9b22a2e7a..00a5b80355 100644 --- a/Content.Shared/Administration/Logs/LogType.cs +++ b/Content.Shared/Administration/Logs/LogType.cs @@ -16,6 +16,9 @@ public enum LogType ShuttleCalled = 8, ShuttleRecalled = 9, ExplosiveDepressurization = 10, + Respawn = 13, + RoundStartJoin = 14, + LateJoin = 15, ChemicalReaction = 17, ReagentEffect = 18, CanisterValve = 20, diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs index ee8f1902ba..e1b785fd70 100644 --- a/Content.Shared/GameTicking/SharedGameTicker.cs +++ b/Content.Shared/GameTicking/SharedGameTicker.cs @@ -1,6 +1,7 @@  using System; using System.Collections.Generic; +using Content.Shared.Station; using Robust.Shared.GameObjects; using Robust.Shared.Network; using Robust.Shared.Serialization; @@ -11,8 +12,8 @@ namespace Content.Shared.GameTicking { // See ideally these would be pulled from the job definition or something. // But this is easier, and at least it isn't hardcoded. - public const string OverflowJob = "Assistant"; - public const string OverflowJobName = "assistant"; + public const string FallbackOverflowJob = "Assistant"; + public const string FallbackOverflowJobName = "assistant"; } [Serializable, NetSerializable] @@ -109,11 +110,13 @@ namespace Content.Shared.GameTicking /// /// The Status of the Player in the lobby (ready, observer, ...) /// - public string[] JobsAvailable { get; } + public Dictionary> JobsAvailableByStation { get; } + public Dictionary StationNames { get; } - public TickerJobsAvailableEvent(string[] jobsAvailable) + public TickerJobsAvailableEvent(Dictionary stationNames, Dictionary> jobsAvailableByStation) { - JobsAvailable = jobsAvailable; + StationNames = stationNames; + JobsAvailableByStation = jobsAvailableByStation; } } diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index 27d3b0d52e..b7d059c3c8 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -99,7 +99,7 @@ namespace Content.Shared.Preferences BackpackPreference.Backpack, new Dictionary { - {SharedGameTicker.OverflowJob, JobPriority.High} + {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.SpawnAsOverflow, new List()); @@ -120,7 +120,7 @@ namespace Content.Shared.Preferences return new HumanoidCharacterProfile(name, age, sex, gender, HumanoidCharacterAppearance.Random(sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, new Dictionary { - {SharedGameTicker.OverflowJob, JobPriority.High} + {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.StayInLobby, new List()); } diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index d11d88ff1c..8a65d047ce 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -43,20 +43,6 @@ namespace Content.Shared.Roles [DataField("head")] public bool IsHead { get; private set; } - /// - /// The total amount of people that can start with this job round-start. - /// - public int SpawnPositions => _spawnPositions ?? TotalPositions; - - [DataField("spawnPositions")] - private int? _spawnPositions; - - /// - /// The total amount of positions available. - /// - [DataField("positions")] - public int TotalPositions { get; private set; } - [DataField("startingGear")] public string? StartingGear { get; private set; } diff --git a/Content.Shared/Station/StationId.cs b/Content.Shared/Station/StationId.cs new file mode 100644 index 0000000000..2e13fa1253 --- /dev/null +++ b/Content.Shared/Station/StationId.cs @@ -0,0 +1,10 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.Station; + +[NetSerializable, Serializable] +public readonly record struct StationId(uint Id) +{ + public static StationId Invalid => new(0); +} diff --git a/Content.Tests/Server/Preferences/ServerDbSqliteTests.cs b/Content.Tests/Server/Preferences/ServerDbSqliteTests.cs index 5c7112cb34..5fd821988c 100644 --- a/Content.Tests/Server/Preferences/ServerDbSqliteTests.cs +++ b/Content.Tests/Server/Preferences/ServerDbSqliteTests.cs @@ -57,7 +57,7 @@ namespace Content.Tests.Server.Preferences BackpackPreference.Backpack, new Dictionary { - {SharedGameTicker.OverflowJob, JobPriority.High} + {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.StayInLobby, new List () diff --git a/Resources/Locale/en-US/late-join/late-join-gui.ftl b/Resources/Locale/en-US/late-join/late-join-gui.ftl index 298244b244..56e53fd034 100644 --- a/Resources/Locale/en-US/late-join/late-join-gui.ftl +++ b/Resources/Locale/en-US/late-join/late-join-gui.ftl @@ -1,3 +1,5 @@ late-join-gui-title = Late Join late-join-gui-jobs-amount-in-department-tooltip = Jobs in the {$departmentName} department -late-join-gui-department-jobs-label = {$departmentName} jobs \ No newline at end of file +late-join-gui-department-jobs-label = {$departmentName} jobs +late-join-gui-job-slot-capped = {$jobName} ({$amount} open) +late-join-gui-job-slot-uncapped = {$jobName} (No limit) diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index a7cbbc3302..7b22106c62 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -98,8 +98,8 @@ idCard: ClownIDCard penSlot: startingItem: CrayonOrange # no pink crayon?!? - # Maybe this is a bad idea. - # At least they can't just spam alt-click it. + # Maybe this is a bad idea. + # At least they can't just spam alt-click it. # You need to remove the ID & alternate between inserting and ejecting ejectSound: /Audio/Items/bikehorn.ogg whitelist: @@ -152,35 +152,35 @@ - type: Icon state: pda-mime -#- type: entity -# name: Chaplain PDA -# parent: BasePDA -# id: ChaplainPDA -# description: God's chosen PDA. -# components: -# - type: PDA -# idCard: ChaplainIDCard -# - type: Appearance -# visuals: -# - type: PDAVisualizer -# state: pda-chaplain -# - type: Icon -# state: pda-chaplain +- type: entity + name: Chaplain PDA + parent: BasePDA + id: ChaplainPDA + description: God's chosen PDA. + components: + - type: PDA + idCard: ChaplainIDCard + - type: Appearance + visuals: + - type: PDAVisualizer + state: pda-chaplain + - type: Icon + state: pda-chaplain -#- type: entity -# name: Quartermaster PDA -# parent: BasePDA -# id: QuartermasterPDA -# description: PDA for the guy that orders the guns. -# components: -# - type: PDA -# idCard: QuartermasterIDCard -# - type: Appearance -# visuals: -# - type: PDAVisualizer -# state: pda-qm -# - type: Icon -# state: pda-qm +- type: entity + name: Quartermaster PDA + parent: BasePDA + id: QuartermasterPDA + description: PDA for the guy that orders the guns. + components: + - type: PDA + idCard: QuartermasterIDCard + - type: Appearance + visuals: + - type: PDAVisualizer + state: pda-qm + - type: Icon + state: pda-qm - type: entity parent: BasePDA @@ -291,7 +291,6 @@ - type: Icon state: pda-engineer - - type: entity parent: BasePDA id: CMOPDA @@ -382,20 +381,20 @@ - type: Icon state: pda-hos -# - type: entity -# parent: BasePDA -# id: WardenPDA -# name: warden PDA -# description: The OS appears to have been jailbroken. -# components: -# - type: PDA -# idCard: WardenIDCard -# - type: Appearance -# visuals: -# - type: PDAVisualizer -# state: pda-warden -# - type: Icon -# state: pda-warden +- type: entity + parent: BasePDA + id: WardenPDA + name: warden PDA + description: The OS appears to have been jailbroken. + components: + - type: PDA + idCard: WardenIDCard + - type: Appearance + visuals: + - type: PDAVisualizer + state: pda-warden + - type: Icon + state: pda-warden - type: entity parent: BasePDA diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml index db413e30f4..fda744465b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml @@ -56,17 +56,17 @@ - type: PresetIdCard job: SecurityOfficer -# - type: entity -# parent: IDCardStandard -# id: WardenIDCard -# name: warden ID card -# components: -# - type: Sprite -# layers: -# - state: default -# - state: idwarden -# - type: PresetIdCard -# job: Warden +- type: entity + parent: IDCardStandard + id: WardenIDCard + name: warden ID card + components: + - type: Sprite + layers: + - state: default + - state: idwarden + - type: PresetIdCard + job: Warden - type: entity parent: IDCardStandard @@ -116,17 +116,17 @@ - type: PresetIdCard job: CargoTechnician -#- type: entity -# parent: IDCardStandard -# id: QuartermasterIDCard -# name: quartermaster ID card -# components: -# - type: Sprite -# layers: -# - state: default -# - state: idquartermaster -# - type: PresetIdCard -# job: Quartermaster +- type: entity + parent: IDCardStandard + id: QuartermasterIDCard + name: quartermaster ID card + components: + - type: Sprite + layers: + - state: default + - state: idquartermaster + - type: PresetIdCard + job: Quartermaster - type: entity parent: IDCardStandard @@ -164,19 +164,17 @@ - type: PresetIdCard job: Mime -#- type: entity -# parent: IDCardStandard -# id: ChaplainIDCard -# name: chaplain ID card -# components: -# - type: Sprite -# layers: -# - state: default -# -# - state: idchaplain -# -# - type: PresetIdCard -# job: Chaplain +- type: entity + parent: IDCardStandard + id: ChaplainIDCard + name: chaplain ID card + components: + - type: Sprite + layers: + - state: default + - state: idchaplain + - type: PresetIdCard + job: Chaplain - type: entity parent: IDCardStandard diff --git a/Resources/Prototypes/Maps/game.yml b/Resources/Prototypes/Maps/game.yml index 53656f8093..b45aeab570 100644 --- a/Resources/Prototypes/Maps/game.yml +++ b/Resources/Prototypes/Maps/game.yml @@ -5,12 +5,57 @@ minPlayers: 0 maxPlayers: 20 fallback: true + overflowJobs: + - Assistant + availableJobs: + CargoTechnician: [ 1, 2 ] + Assistant: [ -1, -1 ] + Bartender: [ 1, 1 ] + Botanist: [ 2, 2 ] + Chef: [ 1, 1 ] + Clown: [ 1, 1 ] + Janitor: [ 1, 1 ] + Mime: [ 1, 1 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 2, 3 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 2, 3 ] + Chemist: [ 1, 1 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 2, 3 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 2, 3 ] - type: gameMap id: packedstation mapName: Packedstation mapPath: Maps/packedstation.yml minPlayers: 15 + overflowJobs: + - Assistant + availableJobs: + CargoTechnician: [ 2, 3 ] + Assistant: [ -1, -1 ] + Bartender: [ 1, 1 ] + Botanist: [ 2, 2 ] + Chef: [ 1, 1 ] + Clown: [ 1, 1 ] + Janitor: [ 1, 1 ] + Mime: [ 1, 1 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 4, 6 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 3, 4 ] + Chemist: [ 2, 2 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 3, 4 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 2, 3 ] + Chaplain: [ 1, 1 ] - type: gameMap id: knightship @@ -18,3 +63,13 @@ mapPath: Maps/knightship.yml minPlayers: 0 maxPlayers: 8 + overflowJobs: [] + availableJobs: + Bartender: [ 1, 1 ] + Captain: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 1, 1 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 1, 1 ] + ResearchDirector: [ 1, 1 ] + Botanist: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/test.yml b/Resources/Prototypes/Maps/test.yml index 34e4af3aa1..30b9b99e02 100644 --- a/Resources/Prototypes/Maps/test.yml +++ b/Resources/Prototypes/Maps/test.yml @@ -3,5 +3,8 @@ mapName: Empty mapPath: Maps/Test/empty.yml minPlayers: 0 - maxPlayers: 0 votable: false + overflowJobs: + - Assistant + availableJobs: + Assistant: [ -1, -1 ] diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml b/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml index 49e7e91096..ac15a7960c 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml @@ -1,8 +1,6 @@ - type: job id: CargoTechnician name: "cargo technician" - positions: 2 - spawnPositions: 1 startingGear: CargoTechGear departments: - Cargo diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml index 0ac51c4f3b..412beadc5b 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml @@ -1,27 +1,25 @@ -#- type: job -# id: Quartermaster -# name: "quartermaster" -# positions: 1 -# spawnPositions: 1 -# startingGear: QuartermasterGear -# departments: -# - Cargo -# icon: "QuarterMaster" -# supervisors: "the head of personnel" -# access: -# - Cargo -# - Quartermaster -# - Maintenance -# -#- type: startingGear -# id: QuartermasterGear -# equipment: -# head: ClothingHeadHatCargosoft -# innerclothing: ClothingUniformJumpsuitQM -# backpack: ClothingBackpackFilled -# shoes: ClothingShoesColorBrown -# idcard: QuartermasterPDA -# ears: ClothingHeadsetCargo -# innerclothingskirt: ClothingUniformJumpskirtQM -# satchel: ClothingBackpackSatchelFilled -# duffelbag: ClothingBackpackDuffelFilled +- type: job + id: Quartermaster + name: "quartermaster" + startingGear: QuartermasterGear + departments: + - Cargo + icon: "QuarterMaster" + supervisors: "the head of personnel" + access: + - Cargo + - Quartermaster + - Maintenance + +- type: startingGear + id: QuartermasterGear + equipment: + head: ClothingHeadHatCargosoft + innerclothing: ClothingUniformJumpsuitQM + backpack: ClothingBackpackFilled + shoes: ClothingShoesColorBrown + idcard: QuartermasterPDA + ears: ClothingHeadsetCargo + innerclothingskirt: ClothingUniformJumpskirtQM + satchel: ClothingBackpackSatchelFilled + duffelbag: ClothingBackpackDuffelFilled diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml b/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml index 94e76d412c..58a4537872 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml @@ -1,7 +1,6 @@ - type: job id: Assistant name: "assistant" - positions: -1 # Treated as infinite. startingGear: AssistantGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml index 750be095d6..072ce54edf 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml @@ -1,7 +1,6 @@ - type: job id: Bartender name: "bartender" - positions: 1 startingGear: BartenderGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml b/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml index 389c9c2c5d..db78ea7d0f 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml @@ -1,8 +1,6 @@ - type: job id: Botanist name: "botanist" - positions: 2 - spawnPositions: 2 startingGear: BotanistGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml index 59d23a874f..64bcbe477a 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml @@ -1,22 +1,21 @@ -#- type: job -# id: Chaplain -# name: "chaplain" -# positions: 1 -# startingGear: ChaplainGear -# departments: -# - Civilian -# icon: "Chaplain" -# supervisors: "the head of personnel" -# access: -# - Chapel -# - Maintenance -#- type: startingGear -# id: ChaplainGear -# equipment: -# innerclothing: ClothingUniformJumpsuitChaplain -# backpack: ClothingBackpack -# shoes: ClothingShoesColorBlack -# idcard: ChaplainPDA -# ears: ClothingHeadsetService -# innerclothingskirt: ClothingUniformJumpskirtChaplain +- type: job + id: Chaplain + name: "chaplain" + startingGear: ChaplainGear + departments: + - Civilian + icon: "Chaplain" + supervisors: "the head of personnel" + access: + - Chapel + - Maintenance +- type: startingGear + id: ChaplainGear + equipment: + innerclothing: ClothingUniformJumpsuitChaplain + backpack: ClothingBackpack + shoes: ClothingShoesColorBlack + idcard: ChaplainPDA + ears: ClothingHeadsetService + innerclothingskirt: ClothingUniformJumpskirtChaplain diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml index f7a3976c3b..160f32908a 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml @@ -1,7 +1,6 @@ - type: job id: Chef name: "chef" - positions: 1 startingGear: ChefGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index dd0ed3669e..31ab7cc5dd 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -1,7 +1,6 @@ - type: job id: Clown name: "clown" - positions: 1 startingGear: ClownGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml b/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml index 31c1003fb9..f45118b8e9 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml @@ -1,7 +1,6 @@ - type: job id: Janitor name: "janitor" - positions: 1 startingGear: JanitorGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml index b1a420d342..bfde71f7a8 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml @@ -1,7 +1,6 @@ - type: job id: Mime name: "mime" - positions: 1 startingGear: MimeGear departments: - Civilian diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index 17af3f12a7..4eaad5230f 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -2,7 +2,6 @@ id: Captain name: "captain" head: true - positions: 1 startingGear: CaptainGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Command/centcom_official.yml b/Resources/Prototypes/Roles/Jobs/Command/centcom_official.yml index b1c1f71de8..e6f4cf97d4 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/centcom_official.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/centcom_official.yml @@ -1,3 +1,15 @@ +- type: job + id: CentralCommandOffical + name: "centcom official" + startingGear: CentcomGear + departments: + - Command + icon: "Nanotrasen" + supervisors: "the head of security" + access: + - Command + - Maintenence + - type: startingGear id: CentcomGear equipment: diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml index b01640bf44..b92b02086e 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml @@ -1,7 +1,6 @@ - type: job id: HeadOfPersonnel name: "head of personnel" - positions: 1 startingGear: HoPGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml index 9fe410d1b3..11ef8097ac 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml @@ -2,7 +2,6 @@ id: ChiefEngineer name: "chief engineer" head: true - positions: 1 startingGear: ChiefEngineerGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml index d4c1efd0d3..803181b778 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml @@ -1,8 +1,6 @@ - type: job id: StationEngineer name: "station engineer" - positions: 3 - spawnPositions: 2 startingGear: StationEngineerGear departments: - Engineering diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml index c2ba174dc1..66d5ce2848 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml @@ -1,8 +1,6 @@ - type: job id: Chemist name: "chemist" - positions: 2 - spawnPositions: 2 startingGear: ChemistGear departments: - Medical diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index f5eae38b15..955ea5e5bf 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -4,7 +4,6 @@ id: ChiefMedicalOfficer name: "chief medical officer" head: true - positions: 1 startingGear: CMOGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml index 59308739b0..4fca77ec92 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml @@ -1,8 +1,6 @@ - type: job id: MedicalDoctor name: "medical doctor" - positions: 3 - spawnPositions: 2 startingGear: DoctorGear departments: - Medical diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index 3cb5621ba2..8c1458665a 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -2,7 +2,6 @@ id: ResearchDirector name: "research director" head: true - positions: 1 startingGear: ResearchDirectorGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml index f00abcf21f..54a3054a1e 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml @@ -1,8 +1,6 @@ - type: job id: Scientist name: "scientist" - positions: 3 - spawnPositions: 2 startingGear: ScientistGear departments: - Science diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml index 258f332194..884adbad13 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml @@ -2,7 +2,6 @@ id: HeadOfSecurity name: "head of security" head: true - positions: 1 startingGear: HoSGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml index c0f554d3f5..40df997ac9 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml @@ -1,8 +1,6 @@ - type: job id: SecurityOfficer name: "security officer" - positions: 3 - spawnPositions: 2 startingGear: SecurityOfficerGear departments: - Security diff --git a/Resources/Prototypes/Roles/Jobs/Security/warden.yml b/Resources/Prototypes/Roles/Jobs/Security/warden.yml index 35144e5e62..fce0c1b199 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/warden.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/warden.yml @@ -1,31 +1,29 @@ -# - type: job -# id: Warden -# name: "warden" -# positions: 1 -# spawnPositions: 1 -# startingGear: WardenGear -# departments: -# - Security -# icon: "Warden" -# supervisors: "the head of security" -# access: -# - Security -# - Brig -# - Maintenance -# - Service -# -# - type: startingGear -# id: WardenGear -# equipment: -# head: ClothingHeadHatWarden -# innerclothing: ClothingUniformJumpsuitWarden -# backpack: ClothingBackpackSecurityFilled -# shoes: ClothingShoesBootsJack -# eyes: ClothingEyesGlassesSecurity -# outerclothing: ClothingOuterCoatWarden -# idcard: WardenPDA -# ears: ClothingHeadsetSecurity -# belt: ClothingBeltSecurityFilled -# innerclothingskirt: ClothingUniformJumpskirtWarden -# satchel: ClothingBackpackSatchelSecurityFilled -# duffelbag: ClothingBackpackDuffelSecurityFilled +- type: job + id: Warden + name: "warden" + startingGear: WardenGear + departments: + - Security + icon: "Warden" + supervisors: "the head of security" + access: + - Security + - Brig + - Maintenance + - Service + +- type: startingGear + id: WardenGear + equipment: + head: ClothingHeadHatWarden + innerclothing: ClothingUniformJumpsuitWarden + backpack: ClothingBackpackSecurityFilled + shoes: ClothingShoesBootsJack + eyes: ClothingEyesGlassesSecurity + outerclothing: ClothingOuterCoatWarden + idcard: WardenPDA + ears: ClothingHeadsetSecurity + belt: ClothingBeltSecurityFilled + innerclothingskirt: ClothingUniformJumpskirtWarden + satchel: ClothingBackpackSatchelSecurityFilled + duffelbag: ClothingBackpackDuffelSecurityFilled