From 36181334b5dbe42f31a893bf5379e4781a8a6e24 Mon Sep 17 00:00:00 2001 From: Moony Date: Tue, 10 May 2022 13:43:30 -0500 Subject: [PATCH] StationSystem/jobs/partial spawning refactor (#7580) * Partial work on StationSystem refactor. * WIP station jobs API. * forgor to fire off grid events. * Partial implementation of StationSpawningSystem * whoops infinite loop. * Spawners should work now. * it compiles. * tfw * Vestigial code cleanup. * fix station deletion. * attempt to make tests go brr * add latejoin spawnpoints to test maps. * make sure the station still exists while destructing spawners. * forgot an exists check. * destruction order check. * hopefully fix final test. * fail-safe radstorm. * Deep-clean job code further. This is bugged!!!!! * Fix job bug. (init order moment) * whooo cleanup * New job selection algorithm that tries to distribute fairly across stations. * small nitpicks * Give the heads their weights to replace the head field. * make overflow assign take a station list. * moment * Fixes and test #1 of many. * please fix nullspace * AssignJobs should no longer even consider showing up on a trace. * add comment. * Introduce station configs, praying i didn't miss something. * in one small change stations are now fully serializable. * Further doc comments. * whoops. * Solve bug where assignjobs didn't account for roundstart. * Fix spawning, improve the API. Caught an oversight in stationsystem that should've broke everything but didn't, whoops. * Goodbye JobController. * minor fix.. * fix test fail, remove debug logs. * quick serialization fixes. * fixes.. * sus * partialing * Update Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs Co-authored-by: Kara * Use dirtying to avoid rebuilding the list 2,100 times. * add a bajillion more lines of docs (mostly in AssignJobs so i don't ever forget how it works) * Update Content.IntegrationTests/Tests/Station/StationJobsTest.cs Co-authored-by: Kara * Add the Mysteriously Missing Captain Check. * Put maprender back the way it belongs. * I love addressing reviews. * Update Content.Server/Station/Systems/StationJobsSystem.cs Co-authored-by: Kara * doc cleanup. * Fix bureaucratic error, add job slot tests. * zero cost abstractions when * cri * saner error. * Fix spawning failing certain tests due to gameticker not handling falliability correctly. Can't fix this until I refactor the rest of spawning code. * submodule gaming * Packedenger. * Documentation consistency. Co-authored-by: Kara --- .../GameTicking/Managers/ClientGameTicker.cs | 11 +- Content.Client/LateJoin/LateJoinGui.cs | 25 +- .../Tests/Station/StationJobsTest.cs | 215 ++++++++ .../AI/Components/AiControllerComponent.cs | 7 +- .../Station/AdjustStationJobCommand.cs | 21 +- .../Station/ListStationJobsCommand.cs | 16 +- .../Commands/Station/ListStationsCommand.cs | 9 +- .../Commands/Station/RenameStationCommand.cs | 9 +- .../GameTicking/Commands/JoinGameCommand.cs | 14 +- .../GameTicking/GameTicker.CVars.cs | 2 +- .../GameTicking/GameTicker.JobController.cs | 196 ------- .../GameTicking/GameTicker.Player.cs | 20 +- .../GameTicking/GameTicker.RoundFlow.cs | 7 +- .../GameTicking/GameTicker.Spawning.cs | 243 +-------- Content.Server/GameTicking/GameTicker.cs | 8 +- .../GameTicking/Rules/SuspicionRuleSystem.cs | 5 +- Content.Server/Maps/GameMapManager.cs | 13 +- .../Maps/GameMapPrototype.MapSelection.cs | 35 ++ Content.Server/Maps/GameMapPrototype.cs | 73 +-- Content.Server/Maps/IGameMapManager.cs | 2 - .../NameGenerators/NanotrasenNameGenerator.cs | 2 +- ...meGenerator.cs => StationNameGenerator.cs} | 2 +- .../EntitySystems/SpawnPointSystem.cs | 57 ++ .../Components/BecomesStationComponent.cs | 6 +- .../Components/PartOfStationComponent.cs | 11 +- .../Station/Components/StationComponent.cs | 13 - .../Components/StationDataComponent.cs | 28 + .../Components/StationJobsComponent.cs | 61 +++ .../Components/StationMemberComponent.cs | 16 + .../Components/StationSpawningComponent.cs | 12 + Content.Server/Station/StationConfig.Jobs.cs | 28 + Content.Server/Station/StationConfig.cs | 33 ++ Content.Server/Station/StationSystem.cs | 276 ---------- .../Systems/StationJobsSystem.Roundstart.cs | 347 ++++++++++++ .../Station/Systems/StationJobsSystem.cs | 498 ++++++++++++++++++ .../Station/Systems/StationSpawningSystem.cs | 206 ++++++++ .../Station/Systems/StationSystem.cs | 407 ++++++++++++++ .../StationEvents/Events/BureaucraticError.cs | 24 +- .../StationEvents/Events/GasLeak.cs | 7 +- .../StationEvents/Events/RadiationStorm.cs | 45 +- .../StationEvents/Events/StationEvent.cs | 29 +- .../GameTicking/SharedGameTicker.cs | 10 +- Content.Shared/Roles/JobPrototype.cs | 4 +- Content.Shared/Station/StationId.cs | 10 - Resources/Maps/Test/empty.yml | 6 + Resources/Maps/Test/floor3x3.yml | 6 + .../Entities/Markers/Spawners/jobs.yml | 3 +- Resources/Prototypes/Maps/bagel.yml | 78 +-- Resources/Prototypes/Maps/delta.yml | 70 +-- Resources/Prototypes/Maps/game.yml | 239 +++------ Resources/Prototypes/Maps/holiday.yml | 72 +-- Resources/Prototypes/Maps/marathon.yml | 78 +-- Resources/Prototypes/Maps/moose.yml | 78 +-- Resources/Prototypes/Maps/packedstation.yml | 40 ++ Resources/Prototypes/Maps/saltern.yml | 89 ++-- Resources/Prototypes/Maps/splitstation.yml | 80 +-- Resources/Prototypes/Maps/test.yml | 12 +- .../Roles/Jobs/Cargo/quartermaster.yml | 4 +- .../Prototypes/Roles/Jobs/Command/captain.yml | 2 +- .../Roles/Jobs/Command/head_of_personnel.yml | 2 +- .../Roles/Jobs/Engineering/chief_engineer.yml | 2 +- .../Jobs/Medical/chief_medical_officer.yml | 2 +- .../Roles/Jobs/Science/research_director.yml | 2 +- .../Roles/Jobs/Security/head_of_security.yml | 2 +- SpaceStation14.sln.DotSettings | 2 + 65 files changed, 2564 insertions(+), 1368 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Station/StationJobsTest.cs delete mode 100644 Content.Server/GameTicking/GameTicker.JobController.cs create mode 100644 Content.Server/Maps/GameMapPrototype.MapSelection.cs rename Content.Server/Maps/NameGenerators/{GameMapNameGenerator.cs => StationNameGenerator.cs} (82%) create mode 100644 Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs delete mode 100644 Content.Server/Station/Components/StationComponent.cs create mode 100644 Content.Server/Station/Components/StationDataComponent.cs create mode 100644 Content.Server/Station/Components/StationJobsComponent.cs create mode 100644 Content.Server/Station/Components/StationMemberComponent.cs create mode 100644 Content.Server/Station/Components/StationSpawningComponent.cs create mode 100644 Content.Server/Station/StationConfig.Jobs.cs create mode 100644 Content.Server/Station/StationConfig.cs delete mode 100644 Content.Server/Station/StationSystem.cs create mode 100644 Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs create mode 100644 Content.Server/Station/Systems/StationJobsSystem.cs create mode 100644 Content.Server/Station/Systems/StationSpawningSystem.cs create mode 100644 Content.Server/Station/Systems/StationSystem.cs delete mode 100644 Content.Shared/Station/StationId.cs create mode 100644 Resources/Prototypes/Maps/packedstation.yml diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs index 7609b71d0c..96b5b8f925 100644 --- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs +++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs @@ -3,7 +3,6 @@ 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; @@ -17,8 +16,8 @@ namespace Content.Client.GameTicking.Managers { [Dependency] private readonly IStateManager _stateManager = default!; [ViewVariables] private bool _initialized; - private Dictionary> _jobsAvailable = new(); - private Dictionary _stationNames = new(); + private Dictionary> _jobsAvailable = new(); + private Dictionary _stationNames = new(); [ViewVariables] public bool AreWeReady { get; private set; } [ViewVariables] public bool IsGameStarted { get; private set; } @@ -29,14 +28,14 @@ namespace Content.Client.GameTicking.Managers [ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public new bool Paused { get; private set; } [ViewVariables] public Dictionary Status { get; private set; } = new(); - [ViewVariables] public IReadOnlyDictionary> JobsAvailable => _jobsAvailable; - [ViewVariables] public IReadOnlyDictionary StationNames => _stationNames; + [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() { diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs index a3709e5033..97d18d54a5 100644 --- a/Content.Client/LateJoin/LateJoinGui.cs +++ b/Content.Client/LateJoin/LateJoinGui.cs @@ -1,20 +1,12 @@ -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.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.Utility; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using static Robust.Client.UserInterface.Controls.BoxContainer; @@ -26,10 +18,10 @@ namespace Content.Client.LateJoin [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IClientConsoleHost _consoleHost = default!; - public event Action<(StationId, string)> SelectedId; + public event Action<(EntityUid, 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; @@ -57,7 +49,7 @@ namespace Content.Client.LateJoin { var (station, jobId) = x; Logger.InfoS("latejoin", $"Late joining as ID: {jobId}"); - _consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)} {station.Id}"); + _consoleHost.ExecuteCommand($"joingame {CommandParsing.Escape(jobId)} {station}"); Close(); }; @@ -209,7 +201,7 @@ namespace Content.Client.LateJoin var jobLabel = new Label { - Text = job.Value >= 0 ? + Text = job.Value != null ? 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)) }; @@ -234,8 +226,9 @@ namespace Content.Client.LateJoin } } - private void JobsAvailableUpdated(IReadOnlyDictionary> _) + private void JobsAvailableUpdated(IReadOnlyDictionary> _) { + Logger.Debug("UI rebuilt."); RebuildUI(); } @@ -255,9 +248,9 @@ namespace Content.Client.LateJoin sealed class JobButton : ContainerButton { public string JobId { get; } - public int Amount { get; } + public uint? Amount { get; } - public JobButton(string jobId, int amount) + public JobButton(string jobId, uint? amount) { JobId = jobId; Amount = amount; diff --git a/Content.IntegrationTests/Tests/Station/StationJobsTest.cs b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs new file mode 100644 index 0000000000..0d737cefdb --- /dev/null +++ b/Content.IntegrationTests/Tests/Station/StationJobsTest.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.Administration.Managers; +using Content.Server.Maps; +using Content.Server.Station.Systems; +using Content.Shared.Preferences; +using NUnit.Framework; +using Robust.Server; +using Robust.Shared.GameObjects; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.IntegrationTests.Tests.Station; + +[TestFixture] +[TestOf(typeof(StationJobsSystem))] +public sealed class StationJobsTest : ContentIntegrationTest +{ + private const string Prototypes = @" +- type: gameMap + id: FooStation + minPlayers: 0 + mapName: FooStation + mapPath: Maps/Tests/empty.yml + stations: + Station: + mapNameTemplate: FooStation + overflowJobs: + - Assistant + availableJobs: + TMime: [0, -1] + TAssistant: [-1, -1] + TCaptain: [5, 5] + TClown: [5, 6] + +- type: job + id: TAssistant + +- type: job + id: TMime + weight: 20 + +- type: job + id: TClown + weight: -10 + +- type: job + id: TCaptain + weight: 10 + +- type: job + id: TChaplain +"; + + private const int StationCount = 100; + private const int CaptainCount = StationCount; + private const int PlayerCount = 2000; + private const int TotalPlayers = PlayerCount + CaptainCount; + + [Test] + public async Task AssignJobsTest() + { + var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes, Options = new ServerOptions() { LoadContentResources = false }}; + var server = StartServer(options); + + await server.WaitIdleAsync(); + + var prototypeManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + var fooStationProto = prototypeManager.Index("FooStation"); + var entSysMan = server.ResolveDependency().EntitySysManager; + var stationJobs = entSysMan.GetEntitySystem(); + var stationSystem = entSysMan.GetEntitySystem(); + + List stations = new(); + await server.WaitPost(() => + { + mapManager.CreateNewMapEntity(MapId.Nullspace); + for (var i = 0; i < StationCount; i++) + { + stations.Add(stationSystem.InitializeNewStation(fooStationProto.Stations["Station"], null, $"Foo {StationCount}")); + } + }); + + await server.WaitAssertion(() => + { + + var fakePlayers = new Dictionary() + .AddJob("TAssistant", JobPriority.Medium, PlayerCount) + .AddPreference("TClown", JobPriority.Low) + .AddPreference("TMime", JobPriority.High) + .WithPlayers( + new Dictionary() + .AddJob("TCaptain", JobPriority.High, CaptainCount) + ); + + var start = new Stopwatch(); + start.Start(); + var assigned = stationJobs.AssignJobs(fakePlayers, stations); + var time = start.Elapsed.TotalMilliseconds; + Logger.Info($"Took {time} ms to distribute {TotalPlayers} players."); + + foreach (var station in stations) + { + var assignedHere = assigned + .Where(x => x.Value.Item2 == station) + .ToDictionary(x => x.Key, x => x.Value); + + // Each station should have SOME players. + Assert.That(assignedHere, Is.Not.Empty); + // And it should have at least the minimum players to be considered a "fair" share, as they're all the same. + Assert.That(assignedHere, Has.Count.GreaterThanOrEqualTo(TotalPlayers/stations.Count), "Station has too few players."); + // And it shouldn't have ALL the players, either. + Assert.That(assignedHere, Has.Count.LessThan(TotalPlayers), "Station has too many players."); + // And there should be *A* captain, as there's one player with captain enabled per station. + Assert.That(assignedHere.Where(x => x.Value.Item1 == "TCaptain").ToList(), Has.Count.EqualTo(1)); + } + + // All clown players have assistant as a higher priority. + Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Not.Contain("TClown")); + // Mime isn't an open job-slot at round-start. + Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Not.Contain("TMime")); + // All players have slots they can fill. + Assert.That(assigned.Values, Has.Count.EqualTo(TotalPlayers), $"Expected {TotalPlayers} players."); + // There must be assistants present. + Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TAssistant")); + // There must be captains present, too. + Assert.That(assigned.Values.Select(x => x.Item1).ToList(), Does.Contain("TCaptain")); + }); + } + + [Test] + public async Task AdjustJobsTest() + { + var options = new ServerContentIntegrationOption {ExtraPrototypes = Prototypes, Options = new ServerOptions() { LoadContentResources = false }}; + var server = StartServer(options); + + await server.WaitIdleAsync(); + + var prototypeManager = server.ResolveDependency(); + var mapManager = server.ResolveDependency(); + var fooStationProto = prototypeManager.Index("FooStation"); + var entSysMan = server.ResolveDependency().EntitySysManager; + var stationJobs = entSysMan.GetEntitySystem(); + var stationSystem = entSysMan.GetEntitySystem(); + + var station = EntityUid.Invalid; + await server.WaitPost(() => + { + mapManager.CreateNewMapEntity(MapId.Nullspace); + station = stationSystem.InitializeNewStation(fooStationProto.Stations["Station"], null, $"Foo Station"); + }); + + await server.WaitAssertion(() => + { + // Verify jobs are/are not unlimited. + Assert.Multiple(() => + { + Assert.That(stationJobs.IsJobUnlimited(station, "TAssistant"), "TAssistant is expected to be unlimited."); + Assert.That(stationJobs.IsJobUnlimited(station, "TMime"), "TMime is expected to be unlimited."); + Assert.That(!stationJobs.IsJobUnlimited(station, "TCaptain"), "TCaptain is expected to not be unlimited."); + Assert.That(!stationJobs.IsJobUnlimited(station, "TClown"), "TClown is expected to not be unlimited."); + }); + Assert.Multiple(() => + { + Assert.That(stationJobs.TrySetJobSlot(station, "TClown", 0), "Could not set TClown to have zero slots."); + Assert.That(stationJobs.TryGetJobSlot(station, "TClown", out var clownSlots), "Could not get the number of TClown slots."); + Assert.That(clownSlots, Is.EqualTo(0)); + Assert.That(!stationJobs.TryAdjustJobSlot(station, "TCaptain", -9999), "Was able to adjust TCaptain by -9999 without clamping."); + Assert.That(stationJobs.TryAdjustJobSlot(station, "TCaptain", -9999, false, true), "Could not adjust TCaptain by -9999."); + Assert.That(stationJobs.TryGetJobSlot(station, "TCaptain", out var captainSlots), "Could not get the number of TCaptain slots."); + Assert.That(captainSlots, Is.EqualTo(0)); + }); + Assert.Multiple(() => + { + Assert.That(stationJobs.TrySetJobSlot(station, "TChaplain", 10, true), "Could not create 10 TChaplain slots."); + stationJobs.MakeJobUnlimited(station, "TChaplain"); + Assert.That(stationJobs.IsJobUnlimited(station, "TChaplain"), "Could not make TChaplain unlimited."); + }); + }); + } +} + +internal static class JobExtensions +{ + public static Dictionary AddJob( + this Dictionary inp, string jobId, JobPriority prio = JobPriority.Medium, + int amount = 1) + { + for (var i = 0; i < amount; i++) + { + inp.Add(new NetUserId(Guid.NewGuid()), HumanoidCharacterProfile.Random().WithJobPriority(jobId, prio)); + } + + return inp; + } + + public static Dictionary AddPreference( + this Dictionary inp, string jobId, JobPriority prio = JobPriority.Medium) + { + return inp.ToDictionary(x => x.Key, x => x.Value.WithJobPriority(jobId, prio)); + } + + public static Dictionary WithPlayers( + this Dictionary inp, + Dictionary second) + { + return new[] {inp, second}.SelectMany(x => x).ToDictionary(x => x.Key, x => x.Value); + } +} diff --git a/Content.Server/AI/Components/AiControllerComponent.cs b/Content.Server/AI/Components/AiControllerComponent.cs index 6b9210b0c3..26f1bc15e3 100644 --- a/Content.Server/AI/Components/AiControllerComponent.cs +++ b/Content.Server/AI/Components/AiControllerComponent.cs @@ -1,6 +1,5 @@ using Content.Server.AI.EntitySystems; -using Content.Server.GameTicking; -using Content.Shared.ActionBlocker; +using Content.Server.Station.Systems; using Content.Shared.Movement.Components; using Content.Shared.Roles; using Robust.Shared.Map; @@ -66,11 +65,11 @@ namespace Content.Server.AI.Components if (StartingGearPrototype != null) { - var gameTicker = EntitySystem.Get(); + var stationSpawning = EntitySystem.Get(); var protoManager = IoCManager.Resolve(); var startingGear = protoManager.Index(StartingGearPrototype); - gameTicker.EquipStartingGear(Owner, startingGear, null); + stationSpawning.EquipStartingGear(Owner, startingGear, null); } } diff --git a/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs b/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs index 32ced8277d..ceeffef430 100644 --- a/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs +++ b/Content.Server/Administration/Commands/Station/AdjustStationJobCommand.cs @@ -1,11 +1,7 @@ -using Content.Server.Station; +using Content.Server.Station.Systems; using Content.Shared.Administration; using Content.Shared.Roles; -using Content.Shared.Station; using Robust.Shared.Console; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Prototypes; namespace Content.Server.Administration.Commands.Station; @@ -27,16 +23,17 @@ public sealed class AdjustStationJobCommand : IConsoleCommand return; } - + var prototypeManager = IoCManager.Resolve(); var stationSystem = EntitySystem.Get(); + var stationJobs = EntitySystem.Get(); - if (!uint.TryParse(args[0], out var station) || !stationSystem.StationInfo.ContainsKey(new StationId(station))) + if (!int.TryParse(args[0], out var stationInt) || !stationSystem.Stations.Contains(new EntityUid(stationInt))) { shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1))); return; } - var prototypeManager = IoCManager.Resolve(); + var station = new EntityUid(stationInt); if (!prototypeManager.TryIndex(args[1], out var job)) { @@ -52,6 +49,12 @@ public sealed class AdjustStationJobCommand : IConsoleCommand return; } - stationSystem.AdjustJobsAvailableOnStation(new StationId(station), job, amount); + if (amount == -1) + { + stationJobs.MakeJobUnlimited(station, job); + return; + } + + stationJobs.TrySetJobSlot(station, job, amount, true); } } diff --git a/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs b/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs index 482579e3f2..04f2b67043 100644 --- a/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs +++ b/Content.Server/Administration/Commands/Station/ListStationJobsCommand.cs @@ -1,15 +1,15 @@ -using Content.Server.Station; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; using Content.Shared.Administration; -using Content.Shared.Station; using Robust.Shared.Console; -using Robust.Shared.GameObjects; -using Robust.Shared.Localization; namespace Content.Server.Administration.Commands.Station; [AdminCommand(AdminFlags.Admin)] public sealed class ListStationJobsCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _entityManager = default!; + public string Command => "lsstationjobs"; public string Description => "Lists all jobs on the given station."; @@ -25,16 +25,18 @@ public sealed class ListStationJobsCommand : IConsoleCommand } var stationSystem = EntitySystem.Get(); + var stationJobs = EntitySystem.Get(); - if (!uint.TryParse(args[0], out var station) || !stationSystem.StationInfo.ContainsKey(new StationId(station))) + if (!int.TryParse(args[0], out var station) || !stationSystem.Stations.Contains(new EntityUid(station))) { shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1))); return; } - foreach (var (job, amount) in stationSystem.StationInfo[new StationId(station)].JobList) + foreach (var (job, amount) in stationJobs.GetJobs(new EntityUid(station))) { - shell.WriteLine($"{job}: {amount}"); + var amountText = amount is null ? "Infinite" : amount.ToString(); + shell.WriteLine($"{job}: {amountText}"); } } } diff --git a/Content.Server/Administration/Commands/Station/ListStationsCommand.cs b/Content.Server/Administration/Commands/Station/ListStationsCommand.cs index 8bdfce0c05..4a6b60e3d3 100644 --- a/Content.Server/Administration/Commands/Station/ListStationsCommand.cs +++ b/Content.Server/Administration/Commands/Station/ListStationsCommand.cs @@ -1,4 +1,6 @@ using Content.Server.Station; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; using Content.Shared.Administration; using Robust.Shared.Console; using Robust.Shared.GameObjects; @@ -8,6 +10,8 @@ namespace Content.Server.Administration.Commands.Station; [AdminCommand(AdminFlags.Admin)] public sealed class ListStationsCommand : IConsoleCommand { + [Dependency] private readonly IEntityManager _entityManager = default!; + public string Command => "lsstations"; public string Description => "List all active stations"; @@ -16,9 +20,10 @@ public sealed class ListStationsCommand : IConsoleCommand public void Execute(IConsoleShell shell, string argStr, string[] args) { - foreach (var (id, station) in EntitySystem.Get().StationInfo) + foreach (var station in EntitySystem.Get().Stations) { - shell.WriteLine($"{id.Id, -2} | {station.Name} | {station.MapPrototype.ID}"); + var name = _entityManager.GetComponent(station).EntityName; + shell.WriteLine($"{station, -10} | {name}"); } } } diff --git a/Content.Server/Administration/Commands/Station/RenameStationCommand.cs b/Content.Server/Administration/Commands/Station/RenameStationCommand.cs index 3d2b37576a..ec213b00af 100644 --- a/Content.Server/Administration/Commands/Station/RenameStationCommand.cs +++ b/Content.Server/Administration/Commands/Station/RenameStationCommand.cs @@ -1,9 +1,6 @@ -using Content.Server.Station; +using Content.Server.Station.Systems; using Content.Shared.Administration; -using Content.Shared.Station; using Robust.Shared.Console; -using Robust.Shared.GameObjects; -using Robust.Shared.Localization; namespace Content.Server.Administration.Commands.Station; @@ -26,12 +23,12 @@ public sealed class RenameStationCommand : IConsoleCommand var stationSystem = EntitySystem.Get(); - if (!uint.TryParse(args[0], out var station) || !stationSystem.StationInfo.ContainsKey(new StationId(station))) + if (!int.TryParse(args[0], out var station) || !stationSystem.Stations.Contains(new EntityUid(station))) { shell.WriteError(Loc.GetString("shell-argument-station-id-invalid", ("index", 1))); return; } - stationSystem.RenameStation(new StationId(station), args[1]); + stationSystem.RenameStation(new EntityUid(station), args[1]); } } diff --git a/Content.Server/GameTicking/Commands/JoinGameCommand.cs b/Content.Server/GameTicking/Commands/JoinGameCommand.cs index 6f743e5a42..86f9651fd7 100644 --- a/Content.Server/GameTicking/Commands/JoinGameCommand.cs +++ b/Content.Server/GameTicking/Commands/JoinGameCommand.cs @@ -1,8 +1,7 @@ -using Content.Server.Station; +using Content.Server.Station.Systems; using Content.Shared.Administration; using Content.Shared.GameTicking; using Content.Shared.Roles; -using Content.Shared.Station; using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -39,6 +38,7 @@ namespace Content.Server.GameTicking.Commands var ticker = EntitySystem.Get(); var stationSystem = EntitySystem.Get(); + var stationJobs = EntitySystem.Get(); if (!ticker.PlayersInLobby.ContainsKey(player) || ticker.PlayersInLobby[player] == LobbyPlayerStatus.Observer) { @@ -56,23 +56,23 @@ namespace Content.Server.GameTicking.Commands { string id = args[0]; - if (!uint.TryParse(args[1], out var sid)) + if (!int.TryParse(args[1], out var sid)) { shell.WriteError(Loc.GetString("shell-argument-must-be-number")); } - var stationId = new StationId(sid); + var station = new EntityUid(sid); var jobPrototype = _prototypeManager.Index(id); - if(!stationSystem.IsJobAvailableOnStation(stationId, jobPrototype)) + if(stationJobs.TryGetJobSlot(station, jobPrototype, out var slots) == false || slots == 0) { shell.WriteLine($"{jobPrototype.Name} has no available slots."); return; } - ticker.MakeJoinGame(player, stationId, id); + ticker.MakeJoinGame(player, station, id); return; } - ticker.MakeJoinGame(player, StationId.Invalid); + ticker.MakeJoinGame(player, EntityUid.Invalid); } } } diff --git a/Content.Server/GameTicking/GameTicker.CVars.cs b/Content.Server/GameTicking/GameTicker.CVars.cs index ba7aa70468..71cef8c42f 100644 --- a/Content.Server/GameTicking/GameTicker.CVars.cs +++ b/Content.Server/GameTicking/GameTicker.CVars.cs @@ -38,7 +38,7 @@ namespace Content.Server.GameTicking _configurationManager.OnValueChanged(CCVars.GameDummyTicker, value => DummyTicker = value, true); _configurationManager.OnValueChanged(CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true); _configurationManager.OnValueChanged(CCVars.GameDisallowLateJoins, - value => { DisallowLateJoin = value; UpdateLateJoinStatus(); UpdateJobsAvailable(); }, true); + value => { DisallowLateJoin = value; UpdateLateJoinStatus(); }, true); _configurationManager.OnValueChanged(CCVars.StationOffset, value => StationOffset = value, true); _configurationManager.OnValueChanged(CCVars.StationRotation, value => StationRotation = value, true); _configurationManager.OnValueChanged(CCVars.MaxStationOffset, value => MaxStationOffset = value, true); diff --git a/Content.Server/GameTicking/GameTicker.JobController.cs b/Content.Server/GameTicking/GameTicker.JobController.cs deleted file mode 100644 index 7252a13fa6..0000000000 --- a/Content.Server/GameTicking/GameTicker.JobController.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -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.Network; -using Robust.Shared.Player; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking -{ - // This code is responsible for the assigning & picking of jobs. - public sealed partial class GameTicker - { - [ViewVariables] - private readonly List _manifest = new(); - - [ViewVariables] - private readonly Dictionary _spawnedPositions = new(); - - private Dictionary AssignJobs(List availablePlayers, - Dictionary profiles) - { - var assigned = new Dictionary(); - - List<(IPlayerSession, List)> GetPlayersJobCandidates(bool heads, JobPriority i) - { - return availablePlayers.Select(player => - { - var profile = profiles[player.UserId]; - - var roleBans = _roleBanManager.GetJobBans(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; - }) - .Where(p => roleBans != null && !roleBans.Contains(p.Key)) - .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; - } - - availablePositions[picked] -= 1; - assigned.Add(candidate, (picked, id)); - break; - } - } - - availablePlayers.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; - } - - private string? PickBestAvailableJob(IPlayerSession playerSession, HumanoidCharacterProfile profile, - StationId station) - { - if (station == StationId.Invalid) - return null; - - var available = _stationSystem.StationInfo[station].JobList; - - bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId) - { - var roleBans = _roleBanManager.GetJobBans(playerSession.UserId); - var filtered = profile.JobPriorities - .Where(p => p.Value == priority) - .Where(p => roleBans != null && !roleBans.Contains(p.Key)) - .Select(p => p.Key) - .ToList(); - - while (filtered.Count != 0) - { - jobId = _robustRandom.Pick(filtered); - if (available.GetValueOrDefault(jobId, 0) > 0) - { - return true; - } - - filtered.Remove(jobId); - } - - jobId = default; - return false; - } - - if (TryPick(JobPriority.High, out var picked)) - { - return picked; - } - - if (TryPick(JobPriority.Medium, out picked)) - { - return picked; - } - - if (TryPick(JobPriority.Low, out picked)) - { - return picked; - } - - var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone().ToList(); - return overflows.Count != 0 ? _robustRandom.Pick(overflows) : null; - } - - [Conditional("DEBUG")] - private void InitializeJobController() - { - // Verify that the overflow role exists and has the correct name. - var role = _prototypeManager.Index(FallbackOverflowJob); - DebugTools.Assert(role.Name == Loc.GetString(FallbackOverflowJobName), - "Overflow role does not have the correct name!"); - } - - private void AddSpawnedPosition(string jobId) - { - _spawnedPositions[jobId] = _spawnedPositions.GetValueOrDefault(jobId, 0) + 1; - } - - private TickerJobsAvailableEvent GetJobsAvailable() - { - // If late join is disallowed, return no available jobs. - if (DisallowLateJoin) - return new TickerJobsAvailableEvent(new Dictionary(), new Dictionary>()); - - var jobs = new Dictionary>(); - var stationNames = new Dictionary(); - - 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); - } - - 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 cf252a8d1c..c7b92b9537 100644 --- a/Content.Server/GameTicking/GameTicker.Player.cs +++ b/Content.Server/GameTicking/GameTicker.Player.cs @@ -1,16 +1,10 @@ -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; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -112,7 +106,7 @@ namespace Content.Server.GameTicking async void SpawnWaitPrefs() { await _prefsManager.WaitPreferencesLoaded(session); - SpawnPlayer(session, StationId.Invalid); + SpawnPlayer(session, EntityUid.Invalid); } async void AddPlayerToDb(Guid id) @@ -151,7 +145,7 @@ namespace Content.Server.GameTicking RaiseNetworkEvent(GetStatusMsg(session), client); RaiseNetworkEvent(GetInfoMsg(), client); RaiseNetworkEvent(GetPlayerStatus(), client); - RaiseNetworkEvent(GetJobsAvailable(), client); + RaiseLocalEvent(new PlayerJoinedLobbyEvent(session)); } private void ReqWindowAttentionAll() @@ -159,4 +153,14 @@ namespace Content.Server.GameTicking RaiseNetworkEvent(new RequestWindowAttentionEvent()); } } + + public sealed class PlayerJoinedLobbyEvent : EntityEventArgs + { + public readonly IPlayerSession PlayerSession; + + public PlayerJoinedLobbyEvent(IPlayerSession playerSession) + { + PlayerSession = playerSession; + } + } } diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 60ac9a094e..39d677562c 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -6,13 +6,11 @@ using Content.Server.Ghost; using Content.Server.Maps; using Content.Server.Mind; using Content.Server.Players; -using Content.Server.Station; using Content.Shared.CCVar; using Content.Shared.Coordinates; using Content.Shared.GameTicking; using Content.Shared.Preferences; using Content.Shared.Sound; -using Content.Shared.Station; using JetBrains.Annotations; using Prometheus; using Robust.Server.Maps; @@ -207,7 +205,7 @@ namespace Content.Server.GameTicking // MapInitialize *before* spawning players, our codebase is too shit to do it afterwards... _mapManager.DoMapInitialize(DefaultMap); - SpawnPlayers(readyPlayers, origReadyPlayers, profiles, force); + SpawnPlayers(readyPlayers, origReadyPlayers.Select(x => x.UserId), profiles, force); _roundStartDateTime = DateTime.UtcNow; RunLevel = GameRunLevel.InRound; @@ -216,7 +214,6 @@ namespace Content.Server.GameTicking SendStatusToAll(); ReqWindowAttentionAll(); UpdateLateJoinStatus(); - UpdateJobsAvailable(); AnnounceRound(); #if EXCEPTION_TOLERANCE @@ -428,8 +425,6 @@ namespace Content.Server.GameTicking // So clients' entity systems can clean up too... RaiseNetworkEvent(ev, Filter.Broadcast()); - _spawnedPositions.Clear(); - _manifest.Clear(); DisallowLateJoin = false; } diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 4d0c26872d..a4b5f91709 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -1,25 +1,17 @@ using System.Globalization; using System.Linq; -using Content.Server.Access.Systems; using Content.Server.Ghost; using Content.Server.Ghost.Components; -using Content.Server.Hands.Components; 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.Access.Components; using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Ghost; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Inventory; -using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Roles; -using Content.Shared.Species; -using Content.Shared.Station; +using JetBrains.Annotations; using Robust.Server.Player; using Robust.Shared.Map; using Robust.Shared.Network; @@ -32,85 +24,35 @@ namespace Content.Server.GameTicking { private const string ObserverPrototypeName = "MobObserver"; - [Dependency] private readonly IdCardSystem _cardSystem = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = 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)] + [ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] private EntityCoordinates _spawnPoint; // Mainly to avoid allocations. private readonly List _possiblePositions = new(); - private void SpawnPlayers(List readyPlayers, IPlayerSession[] origReadyPlayers, + private void SpawnPlayers(List readyPlayers, IEnumerable origReadyPlayers, Dictionary profiles, bool force) { // Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard) RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force)); - var assignedJobs = AssignJobs(readyPlayers, profiles); + var assignedJobs = _stationJobs.AssignJobs(profiles, _stationSystem.Stations.ToList()); - AssignOverflowJobs(assignedJobs, origReadyPlayers, profiles); + _stationJobs.AssignOverflowJobs(ref assignedJobs, origReadyPlayers, profiles, _stationSystem.Stations.ToList()); // Spawn everybody in! foreach (var (player, (job, station)) in assignedJobs) { - SpawnPlayer(player, profiles[player.UserId], station, job, false); + SpawnPlayer(_playerManager.GetSessionByUserId(player), profiles[player], station, job, false); } RefreshLateJoinAllowed(); // Allow rules to add roles to players who have been spawned in. (For example, on-station traitors) - RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.ToArray(), profiles, force)); + RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.Select(x => _playerManager.GetSessionByUserId(x)).ToArray(), profiles, force)); } - private void AssignOverflowJobs(IDictionary assignedJobs, - IPlayerSession[] origReadyPlayers, IReadOnlyDictionary profiles) - { - // For players without jobs, give them the overflow job if they have that set... - foreach (var player in origReadyPlayers) - { - if (assignedJobs.ContainsKey(player)) - { - continue; - } - - var profile = profiles[player.UserId]; - if (profile.PreferenceUnavailable != PreferenceUnavailableMode.SpawnAsOverflow) - continue; - - // Pick a random station - var stations = _stationSystem.StationInfo.Keys.ToList(); - - if (stations.Count == 0) - { - assignedJobs.Add(player, (FallbackOverflowJob, StationId.Invalid)); - continue; - } - - _robustRandom.Shuffle(stations); - - 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])); - break; - } - } - } - - private void SpawnPlayer(IPlayerSession player, StationId station, string? jobId = null, bool lateJoin = true) + private void SpawnPlayer(IPlayerSession player, EntityUid station, string? jobId = null, bool lateJoin = true) { var character = GetPlayerProfile(player); @@ -118,21 +60,20 @@ namespace Content.Server.GameTicking if (jobBans == null || (jobId != null && jobBans.Contains(jobId))) return; SpawnPlayer(player, character, station, jobId, lateJoin); - UpdateJobsAvailable(); } - private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, StationId station, string? jobId = null, bool lateJoin = true) + private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true) { // Can't spawn players with a dummy ticker! if (DummyTicker) return; - if (station == StationId.Invalid) + if (station == EntityUid.Invalid) { - var stations = _stationSystem.StationInfo.Keys.ToList(); + var stations = _stationSystem.Stations.ToList(); _robustRandom.Shuffle(stations); if (stations.Count == 0) - station = StationId.Invalid; + station = EntityUid.Invalid; else station = stations[0]; } @@ -155,7 +96,8 @@ namespace Content.Server.GameTicking } // Pick best job best on prefs. - jobId ??= PickBestAvailableJob(player, character, station); + jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station, character.JobPriorities, true, + _roleBanManager.GetJobBans(player.UserId)); // If no job available, stay in lobby, or if no lobby spawn as observer if (jobId is null) { @@ -194,7 +136,10 @@ namespace Content.Server.GameTicking playDefaultSound: false); } - var mob = SpawnPlayerMob(job, character, station, lateJoin); + var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character); + DebugTools.AssertNotNull(mobMaybe); + var mob = mobMaybe!.Value; + newMind.TransferTo(mob); if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}")) @@ -202,21 +147,12 @@ namespace Content.Server.GameTicking EntityManager.AddComponent(mob); } - AddManifestEntry(character.Name, jobId); - AddSpawnedPosition(jobId); - EquipIdCard(mob, character.Name, jobPrototype); - - foreach (var jobSpecial in jobPrototype.Special) - { - jobSpecial.AfterEquip(mob); - } - - _stationSystem.TryAssignJobToStation(station, jobPrototype); + _stationJobs.TryAssignJob(station, jobPrototype); 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 {ToPrettyString(mob):entity} as a {job.Name:jobName}."); + _adminLogSystem.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} 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 {ToPrettyString(mob):entity} as a {job.Name:jobName}."); + _adminLogSystem.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}."); // We raise this event directed to the mob, but also broadcast it so game rules can do something now. var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, station, character); @@ -231,10 +167,10 @@ namespace Content.Server.GameTicking if (LobbyEnabled) PlayerJoinLobby(player); else - SpawnPlayer(player, StationId.Invalid); + SpawnPlayer(player, EntityUid.Invalid); } - public void MakeJoinGame(IPlayerSession player, StationId station, string? jobId = null) + public void MakeJoinGame(IPlayerSession player, EntityUid station, string? jobId = null) { if (!_playersInLobby.ContainsKey(player)) return; @@ -276,28 +212,6 @@ namespace Content.Server.GameTicking } #region Mob Spawning Helpers - private EntityUid SpawnPlayerMob(Job job, HumanoidCharacterProfile? profile, StationId station, bool lateJoin = true) - { - var coordinates = lateJoin ? GetLateJoinSpawnPoint(station) : GetJobSpawnPoint(job.Prototype.ID, station); - var entity = EntityManager.SpawnEntity( - _prototypeManager.Index(profile?.Species ?? SpeciesManager.DefaultSpecies).Prototype, - coordinates); - - if (job.StartingGear != null) - { - var startingGear = _prototypeManager.Index(job.StartingGear); - EquipStartingGear(entity, startingGear, profile); - } - - if (profile != null) - { - _humanoidAppearanceSystem.UpdateFromProfile(entity, profile); - EntityManager.GetComponent(entity).EntityName = profile.Name; - } - - return entity; - } - private EntityUid SpawnObserverMob() { var coordinates = GetObserverSpawnPoint(); @@ -305,108 +219,7 @@ namespace Content.Server.GameTicking } #endregion - #region Equip Helpers - public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile) - { - if (_inventorySystem.TryGetSlots(entity, out var slotDefinitions)) - { - foreach (var slot in slotDefinitions) - { - var equipmentStr = startingGear.GetGear(slot.Name, profile); - if (!string.IsNullOrEmpty(equipmentStr)) - { - var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent(entity).Coordinates); - _inventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true); - } - } - } - - if (!TryComp(entity, out HandsComponent? handsComponent)) - return; - - var inhand = startingGear.Inhand; - var coords = EntityManager.GetComponent(entity).Coordinates; - foreach (var (hand, prototype) in inhand) - { - var inhandEntity = EntityManager.SpawnEntity(prototype, coords); - _handsSystem.TryPickup(entity, inhandEntity, hand, checkActionBlocker: false, handsComp: handsComponent); - } - } - - public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype) - { - if (!_inventorySystem.TryGetSlotEntity(entity, "id", out var idUid)) - return; - - if (!EntityManager.TryGetComponent(idUid, out PDAComponent? pdaComponent) || pdaComponent.ContainedID == null) - return; - - var card = pdaComponent.ContainedID; - _cardSystem.TryChangeFullName(card.Owner, characterName, card); - _cardSystem.TryChangeJobTitle(card.Owner, jobPrototype.Name, card); - - var access = EntityManager.GetComponent(card.Owner); - var accessTags = access.Tags; - accessTags.UnionWith(jobPrototype.Access); - _pdaSystem.SetOwner(pdaComponent, characterName); - } - #endregion - - private void AddManifestEntry(string characterName, string jobId) - { - _manifest.Add(new ManifestEntry(characterName, jobId)); - } - #region Spawn Points - public EntityCoordinates GetJobSpawnPoint(string jobId, StationId station) - { - var location = _spawnPoint; - - _possiblePositions.Clear(); - - foreach (var (point, transform) in EntityManager.EntityQuery(true)) - { - 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(StationId station) - { - var location = _spawnPoint; - - _possiblePositions.Clear(); - - foreach (var (point, transform) in EntityManager.EntityQuery(true)) - { - 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) - location = _robustRandom.Pick(_possiblePositions); - - return location; - } - - public EntityCoordinates GetObserverSpawnPoint() { var location = _spawnPoint; @@ -432,15 +245,16 @@ namespace Content.Server.GameTicking /// You can use this event to spawn a player off-station on late-join but also at round start. /// When this event is handled, the GameTicker will not perform its own player-spawning logic. /// + [PublicAPI] public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs { public IPlayerSession Player { get; } public HumanoidCharacterProfile Profile { get; } public string? JobId { get; } public bool LateJoin { get; } - public StationId Station { get; } + public EntityUid Station { get; } - public PlayerBeforeSpawnEvent(IPlayerSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, StationId station) + public PlayerBeforeSpawnEvent(IPlayerSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, EntityUid station) { Player = player; Profile = profile; @@ -455,16 +269,17 @@ namespace Content.Server.GameTicking /// You can use this to handle people late-joining, or to handle people being spawned at round start. /// Can be used to give random players a role, modify their equipment, etc. /// + [PublicAPI] public sealed class PlayerSpawnCompleteEvent : EntityEventArgs { public EntityUid Mob { get; } public IPlayerSession Player { get; } public string? JobId { get; } public bool LateJoin { get; } - public StationId Station { get; } + public EntityUid Station { get; } public HumanoidCharacterProfile Profile { get; } - public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, StationId station, HumanoidCharacterProfile profile) + public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, EntityUid station, HumanoidCharacterProfile profile) { Mob = mob; Player = player; diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 1cc672d879..161c3121de 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -7,10 +7,11 @@ using Content.Server.Ghost; using Content.Server.Maps; using Content.Server.PDA; using Content.Server.Preferences.Managers; -using Content.Server.Station; +using Content.Server.Station.Systems; using Content.Shared.Chat; using Content.Shared.Damage; using Content.Shared.GameTicking; +using Content.Shared.Roles; using Robust.Server; using Robust.Server.Maps; using Robust.Server.ServerStatus; @@ -53,8 +54,9 @@ namespace Content.Server.GameTicking InitializeLobbyMusic(); InitializeLobbyBackground(); InitializeGamePreset(); + DebugTools.Assert(_prototypeManager.Index(FallbackOverflowJob).Name == Loc.GetString(FallbackOverflowJobName), + "Overflow role does not have the correct name!"); InitializeGameRules(); - InitializeJobController(); InitializeUpdates(); _initialized = true; @@ -111,6 +113,8 @@ namespace Content.Server.GameTicking [Dependency] private readonly IRuntimeLog _runtimeLog = default!; #endif [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + [Dependency] private readonly StationJobsSystem _stationJobs = default!; [Dependency] private readonly AdminLogSystem _adminLogSystem = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearanceSystem = default!; [Dependency] private readonly PDASystem _pdaSystem = default!; diff --git a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs index c4261eb9c7..2b272790b9 100644 --- a/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs @@ -3,7 +3,7 @@ using System.Threading; using Content.Server.Chat.Managers; using Content.Server.Players; using Content.Server.Roles; -using Content.Server.Station; +using Content.Server.Station.Components; using Content.Server.Suspicion; using Content.Server.Suspicion.Roles; using Content.Server.Traitor.Uplink; @@ -14,7 +14,6 @@ using Content.Shared.EntityList; using Content.Shared.GameTicking; using Content.Shared.Maps; using Content.Shared.MobState.Components; -using Content.Shared.Random.Helpers; using Content.Shared.Roles; using Content.Shared.Sound; using Content.Shared.Suspicion; @@ -222,7 +221,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem var susLoot = _prototypeManager.Index(SuspicionLootTable); - foreach (var (_, mapGrid) in EntityManager.EntityQuery(true)) + foreach (var (_, mapGrid) in EntityManager.EntityQuery(true)) { // I'm so sorry. var tiles = mapGrid.Grid.GetAllTiles().ToArray(); diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index 8777f15f76..0120054113 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Chat.Managers; +using Content.Server.Station; using Content.Shared.CCVar; using Robust.Server.Maps; using Robust.Server.Player; @@ -23,7 +24,7 @@ public sealed class GameMapManager : IGameMapManager [Dependency] private readonly IChatManager _chatManager = default!; [ViewVariables] - private readonly Queue _previousMaps = new Queue(); + private readonly Queue _previousMaps = new(); [ViewVariables] private GameMapPrototype _currentMap = default!; [ViewVariables] @@ -137,7 +138,7 @@ public sealed class GameMapManager : IGameMapManager var map = GetSelectedMap(); if (markAsPlayed) - _previousMaps.Enqueue(map.ID); + EnqueueMap(map.ID); return map; } @@ -158,14 +159,6 @@ public sealed class GameMapManager : IGameMapManager return _prototypeManager.TryIndex(gameMap, out map); } - public string GenerateMapName(GameMapPrototype gameMap) - { - if (gameMap.NameGenerator is not null && gameMap.MapNameTemplate is not null) - return gameMap.NameGenerator.FormatName(gameMap.MapNameTemplate); - else - return gameMap.MapName; - } - public int GetMapQueuePriority(string gameMapProtoName) { var i = 0; diff --git a/Content.Server/Maps/GameMapPrototype.MapSelection.cs b/Content.Server/Maps/GameMapPrototype.MapSelection.cs new file mode 100644 index 0000000000..6ef6644dbe --- /dev/null +++ b/Content.Server/Maps/GameMapPrototype.MapSelection.cs @@ -0,0 +1,35 @@ +namespace Content.Server.Maps; + +public sealed partial class GameMapPrototype +{ + /// + /// 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 voted for. + /// + [DataField("votable")] + public bool Votable { get; } = true; + + /// + /// Minimum players for the given map. + /// + [DataField("minPlayers", required: true)] + public uint MinPlayers { get; } + + /// + /// Maximum players for the given map. + /// + [DataField("maxPlayers")] + public uint MaxPlayers { get; } = uint.MaxValue; + + [DataField("conditions")] private readonly List _conditions = new(); + + /// + /// The game map conditions that must be fulfilled for this map to be selectable. + /// + public IReadOnlyList Conditions => _conditions; +} diff --git a/Content.Server/Maps/GameMapPrototype.cs b/Content.Server/Maps/GameMapPrototype.cs index 2d427c0a78..97ebe3c45c 100644 --- a/Content.Server/Maps/GameMapPrototype.cs +++ b/Content.Server/Maps/GameMapPrototype.cs @@ -1,10 +1,6 @@ -using System.Collections.Generic; -using Content.Server.Maps.NameGenerators; -using Content.Shared.Roles; +using Content.Server.Station; +using JetBrains.Annotations; 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.Utility; namespace Content.Server.Maps; @@ -12,25 +8,17 @@ namespace Content.Server.Maps; /// /// Prototype data for a game map. /// -[Prototype("gameMap")] -public sealed class GameMapPrototype : IPrototype +/// +/// Forks should not directly edit existing parts of this class. +/// Make a new partial for your fancy new feature, it'll save you time later. +/// +[Prototype("gameMap"), PublicAPI] +public sealed partial class GameMapPrototype : IPrototype { /// - [IdDataFieldAttribute] + [IdDataField] public string ID { get; } = default!; - /// - /// Minimum players for the given map. - /// - [DataField("minPlayers", required: true)] - public uint MinPlayers { get; } - - /// - /// Maximum players for the given map. - /// - [DataField("maxPlayers")] - public uint MaxPlayers { get; } = uint.MaxValue; - /// /// Name of the map to use in generic messages, like the map vote. /// @@ -38,49 +26,16 @@ public sealed class GameMapPrototype : IPrototype public string MapName { get; } = default!; /// - /// Name of the given map. - /// - [DataField("mapNameTemplate")] - public string? MapNameTemplate { get; } = default!; - - /// - /// Name generator to use for the map, if any. - /// - [DataField("nameGenerator")] - public GameMapNameGenerator? NameGenerator { get; } = default!; - - /// - /// Relative directory path to the given map, i.e. `Maps/saltern.yml` + /// Relative directory path to the given map, i.e. `/Maps/saltern.yml` /// [DataField("mapPath", required: true)] public ResourcePath MapPath { get; } = default!; - /// - /// Controls if the map can be used as a fallback if no maps are eligible. - /// - [DataField("fallback")] - public bool Fallback { get; } + [DataField("stations")] + private Dictionary _stations = new(); /// - /// Controls if the map can be voted for. + /// The stations this map contains. The names should match with the BecomesStation components. /// - [DataField("votable")] - public bool Votable { get; } = true; - - [DataField("conditions")] - public List Conditions { get; } = new(); - - /// - /// 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!; + public IReadOnlyDictionary Stations => _stations; } diff --git a/Content.Server/Maps/IGameMapManager.cs b/Content.Server/Maps/IGameMapManager.cs index 877e7520b0..a268fb7529 100644 --- a/Content.Server/Maps/IGameMapManager.cs +++ b/Content.Server/Maps/IGameMapManager.cs @@ -64,6 +64,4 @@ public interface IGameMapManager /// name of the map /// existence bool CheckMapExists(string gameMap); - - public string GenerateMapName(GameMapPrototype gameMap); } diff --git a/Content.Server/Maps/NameGenerators/NanotrasenNameGenerator.cs b/Content.Server/Maps/NameGenerators/NanotrasenNameGenerator.cs index 6d3ad86295..b92acc2c83 100644 --- a/Content.Server/Maps/NameGenerators/NanotrasenNameGenerator.cs +++ b/Content.Server/Maps/NameGenerators/NanotrasenNameGenerator.cs @@ -6,7 +6,7 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Maps.NameGenerators; [UsedImplicitly] -public sealed class NanotrasenNameGenerator : GameMapNameGenerator +public sealed class NanotrasenNameGenerator : StationNameGenerator { /// /// Where the map comes from. Should be a two or three letter code, for example "VG" for Packedstation. diff --git a/Content.Server/Maps/NameGenerators/GameMapNameGenerator.cs b/Content.Server/Maps/NameGenerators/StationNameGenerator.cs similarity index 82% rename from Content.Server/Maps/NameGenerators/GameMapNameGenerator.cs rename to Content.Server/Maps/NameGenerators/StationNameGenerator.cs index d69f643736..a0f44466b1 100644 --- a/Content.Server/Maps/NameGenerators/GameMapNameGenerator.cs +++ b/Content.Server/Maps/NameGenerators/StationNameGenerator.cs @@ -3,7 +3,7 @@ using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Maps.NameGenerators; [ImplicitDataDefinitionForInheritors] -public abstract class GameMapNameGenerator +public abstract class StationNameGenerator { public abstract string FormatName(string input); } diff --git a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs b/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs new file mode 100644 index 0000000000..4c177b0d4d --- /dev/null +++ b/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs @@ -0,0 +1,57 @@ +using System.Linq; +using Content.Server.GameTicking; +using Content.Server.Spawners.Components; +using Content.Server.Station.Systems; +using Robust.Shared.Random; + +namespace Content.Server.Spawners.EntitySystems; + +public sealed class SpawnPointSystem : EntitySystem +{ + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnSpawnPlayer); + } + + private void OnSpawnPlayer(PlayerSpawningEvent args) + { + // TODO: Cache all this if it ends up important. + var points = EntityQuery().ToList(); + _random.Shuffle(points); + foreach (var spawnPoint in points) + { + var xform = Transform(spawnPoint.Owner); + if (args.Station != null && _stationSystem.GetOwningStation(spawnPoint.Owner, xform) != args.Station) + continue; + + if (_gameTicker.RunLevel == GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.LateJoin) + { + args.SpawnResult = _stationSpawning.SpawnPlayerMob(xform.Coordinates, args.Job, + args.HumanoidCharacterProfile); + return; + } + else if (_gameTicker.RunLevel != GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.Job && (args.Job == null || spawnPoint.Job?.ID == args.Job.Prototype.ID)) + { + args.SpawnResult = _stationSpawning.SpawnPlayerMob(xform.Coordinates, args.Job, + args.HumanoidCharacterProfile); + return; + } + } + + // Ok we've still not returned, but we need to put them /somewhere/. + // TODO: Refactor gameticker spawning code so we don't have to do this! + foreach (var spawnPoint in points) + { + var xform = Transform(spawnPoint.Owner); + args.SpawnResult = _stationSpawning.SpawnPlayerMob(xform.Coordinates, args.Job, args.HumanoidCharacterProfile); + return; + } + + Logger.ErrorS("spawning", "No spawn points were available!"); + } +} diff --git a/Content.Server/Station/Components/BecomesStationComponent.cs b/Content.Server/Station/Components/BecomesStationComponent.cs index cbfbf5fd5d..f4358dc417 100644 --- a/Content.Server/Station/Components/BecomesStationComponent.cs +++ b/Content.Server/Station/Components/BecomesStationComponent.cs @@ -1,10 +1,6 @@ using Content.Server.GameTicking; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.ViewVariables; -namespace Content.Server.Station; +namespace Content.Server.Station.Components; /// /// Added to grids saved in maps to designate that they are the 'main station' grid. diff --git a/Content.Server/Station/Components/PartOfStationComponent.cs b/Content.Server/Station/Components/PartOfStationComponent.cs index b86adf7591..786f7f3d6c 100644 --- a/Content.Server/Station/Components/PartOfStationComponent.cs +++ b/Content.Server/Station/Components/PartOfStationComponent.cs @@ -1,20 +1,15 @@ using Content.Server.GameTicking; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.ViewVariables; -namespace Content.Server.Station; +namespace Content.Server.Station.Components; /// /// Added to grids saved in maps to designate them as 'part of a station' and not main grids. I.e. ancillary /// shuttles for multi-grid stations. /// -[RegisterComponent] -[Friend(typeof(GameTicker))] +[RegisterComponent, Friend(typeof(GameTicker)), Obsolete("Performs the exact same function as BecomesStationComponent.")] public sealed class PartOfStationComponent : Component { - [DataField("id", required: true)] // does yamllinter even lint maps for required fields? + [DataField("id", required: true)] [ViewVariables(VVAccess.ReadWrite)] public string Id = default!; } diff --git a/Content.Server/Station/Components/StationComponent.cs b/Content.Server/Station/Components/StationComponent.cs deleted file mode 100644 index 2bf681b598..0000000000 --- a/Content.Server/Station/Components/StationComponent.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Content.Shared.Station; -using Robust.Shared.Analyzers; -using Robust.Shared.GameObjects; -using Robust.Shared.ViewVariables; - -namespace Content.Server.Station; - -[RegisterComponent, Friend(typeof(StationSystem))] -public sealed class StationComponent : Component -{ - [ViewVariables] - public StationId Station = StationId.Invalid; -} diff --git a/Content.Server/Station/Components/StationDataComponent.cs b/Content.Server/Station/Components/StationDataComponent.cs new file mode 100644 index 0000000000..0e6807d16e --- /dev/null +++ b/Content.Server/Station/Components/StationDataComponent.cs @@ -0,0 +1,28 @@ +using Content.Server.Maps; +using Content.Server.Station.Systems; +using Robust.Shared.Map; + +namespace Content.Server.Station.Components; + +/// +/// Stores core information about a station, namely it's config and associated grids. +/// All station entities will have this component. +/// +[RegisterComponent, Friend(typeof(StationSystem))] +public sealed class StationDataComponent : Component +{ + /// + /// The game map prototype, if any, associated with this station. + /// + [DataField("stationConfig")] + public StationConfig? StationConfig = null; + + /// + /// List of all grids this station is part of. + /// + /// + /// You should not mutate this yourself, go through StationSystem so the appropriate events get fired. + /// + [DataField("grids")] + public readonly HashSet Grids = new(); +} diff --git a/Content.Server/Station/Components/StationJobsComponent.cs b/Content.Server/Station/Components/StationJobsComponent.cs new file mode 100644 index 0000000000..597d309f02 --- /dev/null +++ b/Content.Server/Station/Components/StationJobsComponent.cs @@ -0,0 +1,61 @@ +using Content.Server.Station.Systems; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; + +namespace Content.Server.Station.Components; + +/// +/// Stores information about a station's job selection. +/// +[RegisterComponent, Friend(typeof(StationJobsSystem)), PublicAPI] +public sealed class StationJobsComponent : Component +{ + /// + /// Total *round-start* jobs at station start. + /// + [DataField("roundStartTotalJobs")] public int RoundStartTotalJobs; + + /// + /// Total *mid-round* jobs at station start. + /// + [DataField("midRoundTotalJobs")] public int MidRoundTotalJobs; + + /// + /// Current total jobs. + /// + [DataField("totalJobs")] public int TotalJobs; + + /// + /// The percentage of jobs remaining. + /// + /// + /// Null if MidRoundTotalJobs is zero. This is a NaN free API. + /// + [ViewVariables] + public float? PercentJobsRemaining => MidRoundTotalJobs > 0 ? TotalJobs / (float) MidRoundTotalJobs : null; + + /// + /// The current list of jobs. + /// + /// + /// 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(); + + /// + /// Overflow jobs that round-start can spawn infinitely many of. + /// + [DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] public HashSet OverflowJobs = new(); +} diff --git a/Content.Server/Station/Components/StationMemberComponent.cs b/Content.Server/Station/Components/StationMemberComponent.cs new file mode 100644 index 0000000000..d9258ec6cb --- /dev/null +++ b/Content.Server/Station/Components/StationMemberComponent.cs @@ -0,0 +1,16 @@ +using Content.Server.Station.Systems; + +namespace Content.Server.Station.Components; + +/// +/// Indicates that a grid is a member of the given station. +/// +[RegisterComponent, Friend(typeof(StationSystem))] +public sealed class StationMemberComponent : Component +{ + /// + /// Station that this grid is a part of. + /// + [ViewVariables] + public EntityUid Station = EntityUid.Invalid; +} diff --git a/Content.Server/Station/Components/StationSpawningComponent.cs b/Content.Server/Station/Components/StationSpawningComponent.cs new file mode 100644 index 0000000000..91da24b754 --- /dev/null +++ b/Content.Server/Station/Components/StationSpawningComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.Station.Systems; +using Robust.Shared.Map; + +namespace Content.Server.Station.Components; + +/// +/// Controls spawning on the given station, tracking spawners present on it. +/// +[RegisterComponent, Friend(typeof(StationSpawningSystem))] +public sealed class StationSpawningComponent : Component +{ +} diff --git a/Content.Server/Station/StationConfig.Jobs.cs b/Content.Server/Station/StationConfig.Jobs.cs new file mode 100644 index 0000000000..d87fafb656 --- /dev/null +++ b/Content.Server/Station/StationConfig.Jobs.cs @@ -0,0 +1,28 @@ +using Content.Shared.Roles; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.Station; + +public sealed partial class StationConfig +{ + [DataField("overflowJobs", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + private readonly List _overflowJobs = default!; + + /// + /// 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 mid-round! + /// + public IReadOnlyList OverflowJobs => _overflowJobs; + + + [DataField("availableJobs", required: true, + customTypeSerializer: typeof(PrototypeIdDictionarySerializer, JobPrototype>))] + private readonly Dictionary> _availableJobs = default!; + + /// + /// Index of all jobs available on the station, of form + /// job name: [round-start, mid-round] + /// + public IReadOnlyDictionary> AvailableJobs => _availableJobs; +} diff --git a/Content.Server/Station/StationConfig.cs b/Content.Server/Station/StationConfig.cs new file mode 100644 index 0000000000..0e46768604 --- /dev/null +++ b/Content.Server/Station/StationConfig.cs @@ -0,0 +1,33 @@ +using Content.Server.Maps.NameGenerators; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.Station; + +/// +/// A config for a station. Specifies name and job slots. +/// This is the only part of stations a downstream should ideally need to modify directly. +/// +/// +/// Forks should not directly edit existing parts of this class. +/// Make a new partial for your fancy new feature, it'll save you time later. +/// +[DataDefinition, PublicAPI] +public sealed partial class StationConfig +{ + /// + /// The name template to use for the station. + /// If there's a name generator this should follow it's required format. + /// + [DataField("mapNameTemplate", required: true)] + public string StationNameTemplate { get; } = default!; + + /// + /// Name generator to use for the station, if any. + /// + [DataField("nameGenerator")] + public StationNameGenerator? NameGenerator { get; } +} + diff --git a/Content.Server/Station/StationSystem.cs b/Content.Server/Station/StationSystem.cs deleted file mode 100644 index 6c8ee31d34..0000000000 --- a/Content.Server/Station/StationSystem.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Linq; -using Content.Server.Chat.Managers; -using Content.Server.GameTicking; -using Content.Server.Maps; -using Content.Shared.CCVar; -using Content.Shared.Roles; -using Content.Shared.Station; -using Robust.Shared.Configuration; -using Robust.Shared.Map; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Station; - -/// -/// System that manages the jobs available on a station, and maybe other things later. -/// -public sealed class StationSystem : EntitySystem -{ - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IConfigurationManager _configurationManager = default!; - [Dependency] private readonly IGameMapManager _gameMapManager = default!; - [Dependency] private readonly ILogManager _logManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly GameTicker _gameTicker = default!; - - private ISawmill _sawmill = default!; - - private uint _idCounter = 1; - - private Dictionary _stationInfo = new(); - - /// - /// List of stations currently loaded. - /// - public IReadOnlyDictionary StationInfo => _stationInfo; - - private bool _randomStationOffset = false; - private bool _randomStationRotation = false; - private float _maxRandomStationOffset = 0.0f; - - public override void Initialize() - { - _sawmill = _logManager.GetSawmill("station"); - - SubscribeLocalEvent(OnRoundEnd); - SubscribeLocalEvent(OnPreGameMapLoad); - SubscribeLocalEvent(OnPostGameMapLoad); - - _configurationManager.OnValueChanged(CCVars.StationOffset, x => _randomStationOffset = x, true); - _configurationManager.OnValueChanged(CCVars.MaxStationOffset, x => _maxRandomStationOffset = x, true); - _configurationManager.OnValueChanged(CCVars.StationRotation, x => _randomStationRotation = x, true); - } - - private void OnPreGameMapLoad(PreGameMapLoad ev) - { - // this is only for maps loaded during round setup! - if (_gameTicker.RunLevel == GameRunLevel.InRound) - return; - - if (_randomStationOffset) - ev.Options.Offset += _random.NextVector2(_maxRandomStationOffset); - - if (_randomStationRotation) - ev.Options.Rotation = _random.NextAngle(); - } - - private void OnPostGameMapLoad(PostGameMapLoad ev) - { - var dict = new Dictionary(); - - // Iterate over all BecomesStation - for (var i = 0; i < ev.Grids.Count; i++) - { - var grid = ev.Grids[i]; - - // We still setup the grid - if (!TryComp(_mapManager.GetGridEuid(grid), out var becomesStation)) - continue; - - var stationId = InitialSetupStationGrid(grid, ev.GameMap, ev.StationName); - - dict.Add(becomesStation.Id, stationId); - } - - if (!dict.Any()) - { - // Oh jeez, no stations got loaded. - // We'll just take the first grid and setup that, then. - - var grid = ev.Grids[0]; - var stationId = InitialSetupStationGrid(grid, ev.GameMap, ev.StationName); - - dict.Add("Station", stationId); - } - - // Iterate over all PartOfStation - for (var i = 0; i < ev.Grids.Count; i++) - { - var grid = ev.Grids[i]; - var geid = _mapManager.GetGridEuid(grid); - if (!TryComp(geid, out var partOfStation)) - continue; - - if (dict.TryGetValue(partOfStation.Id, out var stationId)) - { - AddGridToStation(geid, stationId); - } - else - { - _sawmill.Error($"Grid {grid} ({geid}) specified that it was part of station {partOfStation.Id} which does not exist"); - } - } - } - - /// - /// Cleans up station info. - /// - private void OnRoundEnd(GameRunLevelChangedEvent eventArgs) - { - if (eventArgs.New == GameRunLevel.PreRoundLobby) - _stationInfo = new(); - } - - public sealed class StationInfoData - { - public 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; - } - } - - public bool AdjustJobAmount(string jobName, int amount) - { - DebugTools.Assert(amount >= -1); - _jobList[jobName] = amount; - return true; - } - } - - /// - /// 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 ?? _gameMapManager.GenerateMapName(mapPrototype), mapPrototype, jobListDict); - var station = EntityManager.AddComponent(mapGrid); - station.Station = id; - - _gameTicker.UpdateJobsAvailable(); // new station means new jobs, tell any lobby-goers. - - _sawmill.Info($"Setting up new {mapPrototype.ID} called {_stationInfo[id].Name} 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; - - _sawmill.Info( $"Adding grid {mapGrid}:{gridComponent.GridIndex} to station {station} named {_stationInfo[station].Name}"); - } - - /// - /// Attempts to assign a job on the given station. - /// Does NOT inform the gameticker that the job roster has changed. - /// - /// station to assign to - /// name of the job - /// assignment success - public bool TryAssignJobToStation(StationId stationId, JobPrototype job) - { - if (stationId != StationId.Invalid) - return _stationInfo[stationId].TryAssignJob(job.ID); - else - return false; - } - - /// - /// Checks if the given job is available. - /// - /// station to check - /// name of the job - /// job availability - public bool IsJobAvailableOnStation(StationId stationId, JobPrototype job) - { - if (_stationInfo[stationId].JobList.TryGetValue(job.ID, out var amount)) - return amount != 0; - - return false; - } - - private StationId AllocateStationInfo() - { - return new StationId(_idCounter++); - } - - public bool AdjustJobsAvailableOnStation(StationId stationId, JobPrototype job, int amount) - { - var ret = _stationInfo[stationId].AdjustJobAmount(job.ID, amount); - _gameTicker.UpdateJobsAvailable(); - return ret; - } - - public void RenameStation(StationId stationId, string name, bool loud = true) - { - var oldName = _stationInfo[stationId].Name; - _stationInfo[stationId].Name = name; - if (loud) - { - _chatManager.DispatchStationAnnouncement($"The station {oldName} has been renamed to {name}."); - } - - // Make sure lobby gets the memo. - _gameTicker.UpdateJobsAvailable(); - } -} diff --git a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs new file mode 100644 index 0000000000..094484903c --- /dev/null +++ b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs @@ -0,0 +1,347 @@ +using System.Linq; +using Content.Server.Administration.Managers; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Station.Systems; + +// Contains code for round-start spawning. +public sealed partial class StationJobsSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly RoleBanManager _roleBanManager = default!; + + private Dictionary> _jobsByWeight = default!; + private List _orderedWeights = default!; + + /// + /// Sets up some tables used by AssignJobs, including jobs sorted by their weights, and a list of weights in order from highest to lowest. + /// + private void InitializeRoundStart() + { + _jobsByWeight = new Dictionary>(); + foreach (var job in _prototypeManager.EnumeratePrototypes()) + { + if (!_jobsByWeight.ContainsKey(job.Weight)) + _jobsByWeight.Add(job.Weight, new HashSet()); + + _jobsByWeight[job.Weight].Add(job.ID); + } + + _orderedWeights = _jobsByWeight.Keys.OrderByDescending(i => i).ToList(); + } + + /// + /// Assigns jobs based on the given preferences and list of stations to assign for. + /// This does NOT change the slots on the station, only figures out where each player should go. + /// + /// The profiles to use for selection. + /// List of stations to assign for. + /// Whether or not to use the round-start jobs for the stations instead of their current jobs. + /// List of players and their assigned jobs. + /// + /// You probably shouldn't use useRoundStartJobs mid-round if the station has been available to join, + /// 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) + { + DebugTools.Assert(stations.Count > 0); + + if (profiles.Count == 0) + return new Dictionary(); + + // 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); + + // The jobs left on the stations. This collection is modified as jobs are assigned to track what's available. + var stationJobs = new Dictionary>(); + foreach (var station in stations) + { + if (useRoundStartJobs) + { + stationJobs.Add(station, GetRoundStartJobs(station).ToDictionary(x => x.Key, x => x.Value)); + } + else + { + stationJobs.Add(station, GetJobs(station).ToDictionary(x => x.Key, x => x.Value)); + } + } + + + // We reuse this collection. It tracks what jobs we're currently trying to select players for. + var currentlySelectingJobs = new Dictionary>(stations.Count); + foreach (var station in stations) + { + currentlySelectingJobs.Add(station, new Dictionary()); + } + + // And these. + // Tracks what players are available for a given job in the current iteration of selection. + var jobPlayerOptions = new Dictionary>(); + // 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. + var stationShares = new Dictionary(stations.Count); + + // Ok so the general algorithm: + // We start with the highest weight jobs and work our way down. We filter jobs by weight when selecting as well. + // Weight > Priority > Station. + foreach (var weight in _orderedWeights) + { + for (var selectedPriority = JobPriority.High; selectedPriority > JobPriority.Never; selectedPriority--) + { + if (profiles.Count == 0) + goto endFunc; + + var candidates = GetPlayersJobCandidates(weight, selectedPriority, profiles); + + 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) + { + // Remove the player from all possible jobs as that's faster than actually checking what they have selected. + foreach (var (_, players) in jobPlayerOptions) + { + players.Remove(player); + } + + stationJobs[station][job]--; + profiles.Remove(player); + assigned.Add(player, (job, station)); + + optionsRemaining--; + } + + jobPlayerOptions.Clear(); // We reuse this collection. + + // Goes through every candidate, and adds them to jobPlayerOptions, so that the candidate players + // have an index sorted by job. We use this (much) later when actually assigning people to randomly + // pick from the list of candidates for the job. + foreach (var (user, jobs) in candidates) + { + foreach (var job in jobs) + { + if (!jobPlayerOptions.ContainsKey(job)) + jobPlayerOptions.Add(job, new HashSet()); + + jobPlayerOptions[job].Add(user); + } + + optionsRemaining++; + } + + // We reuse this collection, so clear it's children. + foreach (var slots in currentlySelectingJobs) + { + slots.Value.Clear(); + } + + // Go through every station.. + foreach (var station in stations) + { + var slots = currentlySelectingJobs[station]; + + // Get all of the jobs in the selected weight category. + foreach (var (job, slot) in stationJobs[station]) + { + if (_jobsByWeight[weight].Contains(job)) + slots.Add(job, slot); + } + } + + + // Clear for reuse. + stationTotalSlots.Clear(); + + // Intentionally discounts the value of uncapped slots! They're only a single slot when deciding a station's share. + foreach (var (station, jobs) in currentlySelectingJobs) + { + stationTotalSlots.Add( + station, + (int)jobs.Values.Sum(x => x ?? 1) + ); + } + + var totalSlots = 0; + + // LINQ moment. + // totalSlots = stationTotalSlots.Sum(x => x.Value); + foreach (var (_, slot) in stationTotalSlots) + { + totalSlots += slot; + } + + if (totalSlots == 0) + continue; // No slots so just move to the next iteration. + + // Clear for reuse. + stationShares.Clear(); + + // How many players we've distributed so far. Used to grant any remaining slots if we have leftovers. + var distributed = 0; + + // Goes through each station and figures out how many players we should give it for the current iteration. + foreach (var station in stations) + { + // Calculates the percent share then multiplies. + stationShares[station] = (int)Math.Floor(((float)stationTotalSlots[station] / totalSlots) * candidates.Count); + distributed += stationShares[station]; + } + + // Avoids the fair share problem where if there's two stations and one player neither gets one. + // We do this by simply selecting a station randomly and giving it the remaining share(s). + if (distributed < candidates.Count) + { + var choice = _random.Pick(stations); + stationShares[choice] += candidates.Count - distributed; + } + + // Actual meat, goes through each station and shakes the tree until everyone has a job. + foreach (var station in stations) + { + if (stationShares[station] == 0) + continue; + + // The jobs we're selecting from for the current station. + var currStationSelectingJobs = currentlySelectingJobs[station]; + // We only need this list because we need to go through this in a random order. + // Oh the misery, another allocation. + var allJobs = currStationSelectingJobs.Keys.ToList(); + _random.Shuffle(allJobs); + // And iterates through all it's jobs in a random order until the count settles. + // No, AFAIK it cannot be done any saner than this. I hate "shaking" collections as much + // as you do but it's what seems to be the absolute best option here. + // It doesn't seem to show up on the chart, perf-wise, anyway, so it's likely fine. + int priorCount; + do + { + priorCount = stationShares[station]; + + foreach (var job in allJobs) + { + if (stationShares[station] == 0) + break; + + if (currStationSelectingJobs[job] != null && currStationSelectingJobs[job] == 0) + continue; // Can't assign this job. + + if (!jobPlayerOptions.ContainsKey(job)) + continue; + + // Picking players it finds that have the job set. + var player = _random.Pick(jobPlayerOptions[job]); + AssignPlayer(player, job, station); + stationShares[station]--; + + if (currStationSelectingJobs[job] != null) + currStationSelectingJobs[job]--; + + if (optionsRemaining == 0) + goto done; + } + } while (priorCount != stationShares[station]); + } + done: ; + } + } + + endFunc: + return assigned; + } + + /// + /// Attempts to assign overflow jobs to any player in allPlayersToAssign that is not in assignedJobs. + /// + /// All assigned jobs. + /// 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) + { + var givenStations = stations.ToList(); + if (givenStations.Count == 0) + return; // Don't attempt to assign them if there are no stations. + // For players without jobs, give them the overflow job if they have that set... + foreach (var player in allPlayersToAssign) + { + if (assignedJobs.ContainsKey(player)) + { + continue; + } + + var profile = profiles[player]; + if (profile.PreferenceUnavailable != PreferenceUnavailableMode.SpawnAsOverflow) + continue; + + _random.Shuffle(givenStations); + + foreach (var station in givenStations) + { + // Pick a random overflow job from that station + var overflows = GetOverflowJobs(station).ToList(); + _random.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], givenStations[0])); + break; + } + } + } + + /// + /// Gets all jobs that the input players have that match the given weight and priority. + /// + /// Weight to find, if any. + /// Priority to find, if any. + /// Profiles to look in. + /// Players and a list of their matching jobs. + private Dictionary> GetPlayersJobCandidates(int? weight, JobPriority? selectedPriority, Dictionary profiles) + { + var outputDict = new Dictionary>(profiles.Count); + + foreach (var (player, profile) in profiles) + { + var roleBans = _roleBanManager.GetJobBans(player); + + List? availableJobs = null; + + foreach (var (jobId, priority) in profile.JobPriorities) + { + if (!(priority == selectedPriority || selectedPriority is null)) + continue; + + if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job)) + continue; + + if (weight is not null && job.Weight != weight.Value) + continue; + + if (!(roleBans == null || !roleBans.Contains(jobId))) + continue; + + availableJobs ??= new List(profile.JobPriorities.Count); + + availableJobs.Add(jobId); + } + + if (availableJobs is not null) + outputDict.Add(player, availableJobs); + } + + return outputDict; + } +} diff --git a/Content.Server/Station/Systems/StationJobsSystem.cs b/Content.Server/Station/Systems/StationJobsSystem.cs new file mode 100644 index 0000000000..aefc8e5802 --- /dev/null +++ b/Content.Server/Station/Systems/StationJobsSystem.cs @@ -0,0 +1,498 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.GameTicking; +using Content.Server.Station.Components; +using Content.Shared.CCVar; +using Content.Shared.GameTicking; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Shared.Configuration; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.Station.Systems; + +/// +/// Manages job slots for stations. +/// +[PublicAPI] +public sealed partial class StationJobsSystem : EntitySystem +{ + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnStationInitialized); + SubscribeLocalEvent(OnStationRenamed); + SubscribeLocalEvent(OnStationDeletion); + SubscribeLocalEvent(OnPlayerJoinedLobby); + _configurationManager.OnValueChanged(CCVars.GameDisallowLateJoins, _ => UpdateJobsAvailable(), true); + + InitializeRoundStart(); + } + + public override void Update(float _) + { + if (_availableJobsDirty) + { + _cachedAvailableJobs = GenerateJobsAvailableEvent(); + RaiseNetworkEvent(_cachedAvailableJobs, Filter.Empty().AddPlayers(_gameTicker.PlayersInLobby.Keys)); + } + } + + private void OnStationDeletion(EntityUid uid, StationJobsComponent component, ComponentShutdown args) + { + UpdateJobsAvailable(); // we no longer exist so the jobs list is changed. + } + + private void OnStationInitialized(StationInitializedEvent msg) + { + var stationJobs = AddComp(msg.Station); + var stationData = Comp(msg.Station); + + if (stationData.StationConfig == null) + return; + + var mapJobList = stationData.StationConfig.AvailableJobs; + + 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 = stationData.StationConfig.OverflowJobs.ToHashSet(); + UpdateJobsAvailable(); + } + + #region Public API + + /// + /// Station to assign a job on. + /// Job to assign. + /// Resolve pattern, station jobs component of the station. + public bool TryAssignJob(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null) + { + return TryAssignJob(station, job.ID, stationJobs); + } + + /// + /// Attempts to assign the given job once. (essentially, it decrements the slot if possible). + /// + /// Station to assign a job on. + /// Job prototype ID to assign. + /// Resolve pattern, station jobs component of the station. + /// Whether or not assignment was a success. + /// Thrown when the given station is not a station. + public bool TryAssignJob(EntityUid station, string jobPrototypeId, StationJobsComponent? stationJobs = null) + { + return TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs); + } + + /// + /// Station to adjust the job slot on. + /// Job to adjust. + /// Amount to adjust by. + /// Whether or not it should create the slot if it doesn't exist. + /// Whether or not to clamp to zero if you'd remove more jobs than are available. + /// Resolve pattern, station jobs component of the station. + public bool TryAdjustJobSlot(EntityUid station, JobPrototype job, int amount, bool createSlot = false, bool clamp = false, + StationJobsComponent? stationJobs = null) + { + return TryAdjustJobSlot(station, job.ID, amount, createSlot, clamp, stationJobs); + } + + /// + /// Attempts to adjust the given job slot by the amount provided. + /// + /// Station to adjust the job slot on. + /// Job prototype ID to adjust. + /// Amount to adjust by. + /// Whether or not it should create the slot if it doesn't exist. + /// Whether or not to clamp to zero if you'd remove more jobs than are available. + /// 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, + StationJobsComponent? stationJobs = null) + { + if (!Resolve(station, ref stationJobs)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + var jobList = stationJobs.JobList; + + // This should: + // - Return true when zero slots are added/removed. + // - Return true when you add. + // - Return true when you remove and do not exceed the number of slot available. + // - 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)) + { + case false when amount < 0: + return false; + case false: + if (!createSlot) + return false; + stationJobs.TotalJobs += amount; + jobList[jobPrototypeId] = (uint?)amount; + UpdateJobsAvailable(); + return true; + case true: + // Job is unlimited so just say we adjusted it and do nothing. + if (jobList[jobPrototypeId] == null) + return true; + + // Would remove more jobs than we have available. + if (amount < 0 && (jobList[jobPrototypeId] + 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); + } + + UpdateJobsAvailable(); + return true; + } + } + + /// + /// Station to adjust the job slot on. + /// Job prototype to adjust. + /// Amount to set to. + /// Whether or not it should create the slot if it doesn't exist. + /// Resolve pattern, station jobs component of the station. + /// + public bool TrySetJobSlot(EntityUid station, JobPrototype jobPrototype, int amount, bool createSlot = false, + StationJobsComponent? stationJobs = null) + { + return TrySetJobSlot(station, jobPrototype.ID, amount, createSlot, stationJobs); + } + + /// + /// Attempts to set the given job slot to the amount provided. + /// + /// Station to adjust the job slot on. + /// Job prototype ID to adjust. + /// Amount to set to. + /// Whether or not it should create the slot if it doesn't exist. + /// 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, + StationJobsComponent? stationJobs = null) + { + if (!Resolve(station, ref stationJobs)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + if (amount < 0) + throw new ArgumentException("Tried to set a job to have a negative number of slots!", nameof(amount)); + + var jobList = stationJobs.JobList; + + switch (jobList.ContainsKey(jobPrototypeId)) + { + case false: + if (!createSlot) + return false; + stationJobs.TotalJobs += amount; + jobList[jobPrototypeId] = (uint?)amount; + UpdateJobsAvailable(); + return true; + case true: + // Job is unlimited so just say we adjusted it and do nothing. + if (jobList[jobPrototypeId] == null) + return true; + + stationJobs.TotalJobs += amount - (int)jobList[jobPrototypeId]!.Value; + + jobList[jobPrototypeId] = (uint)amount; + UpdateJobsAvailable(); + return true; + } + } + + /// + /// Station to make a job unlimited on. + /// Job to make unlimited. + /// Resolve pattern, station jobs component of the station. + public void MakeJobUnlimited(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null) + { + MakeJobUnlimited(station, job.ID, stationJobs); + } + + /// + /// Makes the given job have unlimited slots. + /// + /// Station to make a job unlimited on. + /// Job prototype ID to make unlimited. + /// Resolve pattern, station jobs component of the station. + /// Thrown when the given station is not a station. + public void MakeJobUnlimited(EntityUid station, string jobPrototypeId, StationJobsComponent? stationJobs = null) + { + if (!Resolve(station, ref stationJobs)) + 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; + + stationJobs.JobList[jobPrototypeId] = null; + + UpdateJobsAvailable(); + } + + /// + /// Station to check. + /// Job to check. + /// Resolve pattern, station jobs component of the station. + public bool IsJobUnlimited(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null) + { + return IsJobUnlimited(station, job.ID, stationJobs); + } + + /// + /// Checks if the given job is unlimited. + /// + /// Station to check. + /// Job prototype ID to check. + /// Resolve pattern, station jobs component of the station. + /// Returns if the given slot is unlimited. + /// Thrown when the given station is not a station. + public bool IsJobUnlimited(EntityUid station, string jobPrototypeId, StationJobsComponent? stationJobs = null) + { + 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; + } + + /// + /// Station to get slot info from. + /// 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) + { + return TryGetJobSlot(station, job.ID, out slots, stationJobs); + } + + /// + /// Returns information about the given job slot. + /// + /// Station to get slot info from. + /// Job prototype ID to get slot info for. + /// The number of slots remaining. Null if infinite. + /// Resolve pattern, station jobs component of the station. + /// 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) + { + 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; + } + } + + /// + /// Returns all jobs available on the station. + /// + /// Station to get jobs for + /// 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) + { + 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(); + } + + /// + /// Returns all overflow jobs available on the station. + /// + /// Station to get jobs for + /// 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) + { + if (!Resolve(station, ref stationJobs)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + return stationJobs.OverflowJobs.ToHashSet(); + } + + /// + /// Returns a readonly dictionary of all jobs and their slot info. + /// + /// Station to get jobs for + /// 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) + { + if (!Resolve(station, ref stationJobs)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + return stationJobs.JobList; + } + + /// + /// Returns a readonly dictionary of all round-start jobs and their slot info. + /// + /// Station to get jobs for + /// 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) + { + if (!Resolve(station, ref stationJobs)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + return stationJobs.RoundStartJobList; + } + + /// + /// Looks at the given priority list, and picks the best available job (optionally with the given exclusions) + /// + /// Station to pick from. + /// The priority list to use for selecting a job. + /// 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) + { + if (station == EntityUid.Invalid) + return null; + + var available = GetAvailableJobs(station); + bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId) + { + var filtered = jobPriorities + .Where(p => + p.Value == priority + && disallowedJobs != null + && !disallowedJobs.Contains(p.Key) + && available.Contains(p.Key)) + .Select(p => p.Key) + .ToList(); + + if (filtered.Count != 0) + { + jobId = _random.Pick(filtered); + return true; + } + + jobId = default; + return false; + } + + if (TryPick(JobPriority.High, out var picked)) + { + return picked; + } + + if (TryPick(JobPriority.Medium, out picked)) + { + return picked; + } + + if (TryPick(JobPriority.Low, out picked)) + { + return picked; + } + + if (!pickOverflows) + return null; + + var overflows = GetOverflowJobs(station); + return overflows.Count != 0 ? _random.Pick(overflows) : null; + } + + #endregion Public API + + #region Latejoin job management + + private bool _availableJobsDirty; + + private TickerJobsAvailableEvent _cachedAvailableJobs = new (new Dictionary(), new Dictionary>()); + + /// + /// Assembles an event from the current available-to-play jobs. + /// This is moderately expensive to construct. + /// + /// The event. + private TickerJobsAvailableEvent GenerateJobsAvailableEvent() + { + // If late join is disallowed, return no available jobs. + if (_gameTicker.DisallowLateJoin) + return new TickerJobsAvailableEvent(new Dictionary(), new Dictionary>()); + + var jobs = new Dictionary>(); + var stationNames = new Dictionary(); + + foreach (var station in _stationSystem.Stations) + { + var list = Comp(station).JobList.ToDictionary(x => x.Key, x => x.Value); + jobs.Add(station, list); + stationNames.Add(station, Name(station)); + } + return new TickerJobsAvailableEvent(stationNames, jobs); + } + + /// + /// Updates the cached available jobs. Moderately expensive. + /// + private void UpdateJobsAvailable() + { + _availableJobsDirty = true; + } + + private void OnPlayerJoinedLobby(PlayerJoinedLobbyEvent ev) + { + RaiseNetworkEvent(_cachedAvailableJobs, ev.PlayerSession.ConnectedClient); + } + + private void OnStationRenamed(EntityUid uid, StationJobsComponent component, StationRenamedEvent args) + { + UpdateJobsAvailable(); + } + + #endregion +} diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs new file mode 100644 index 0000000000..5e4fd14686 --- /dev/null +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -0,0 +1,206 @@ +using Content.Server.Access.Systems; +using Content.Server.CharacterAppearance.Systems; +using Content.Server.Hands.Components; +using Content.Server.Hands.Systems; +using Content.Server.PDA; +using Content.Server.Roles; +using Content.Server.Station.Components; +using Content.Shared.Access.Components; +using Content.Shared.Inventory; +using Content.Shared.PDA; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using Content.Shared.Species; +using JetBrains.Annotations; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Station.Systems; + +/// +/// Manages spawning into the game, tracking available spawn points. +/// Also provides helpers for spawning in the player's mob. +/// +[PublicAPI] +public sealed class StationSpawningSystem : EntitySystem +{ + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly HandsSystem _handsSystem = default!; + [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearanceSystem = default!; + [Dependency] private readonly IdCardSystem _cardSystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly PDASystem _pdaSystem = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnStationInitialized); + } + + private void OnStationInitialized(StationInitializedEvent ev) + { + AddComp(ev.Station); + } + + /// + /// Attempts to spawn a player character onto the given station. + /// + /// Station to spawn onto. + /// The job to assign, if any. + /// The character profile to use, if any. + /// Resolve pattern, the station spawning component for the station. + /// The resulting player character, if any. + /// Thrown when the given station is not a station. + /// + /// This only spawns the character, and does none of the mind-related setup you'd need for it to be playable. + /// + public EntityUid? SpawnPlayerCharacterOnStation(EntityUid? station, Job? job, HumanoidCharacterProfile? profile, StationSpawningComponent? stationSpawning = null) + { + if (station != null && !Resolve(station.Value, ref stationSpawning)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + var ev = new PlayerSpawningEvent(job, profile, station); + RaiseLocalEvent(ev); + + DebugTools.Assert(ev.SpawnResult is {Valid: true} or null); + + return ev.SpawnResult; + } + + //TODO: Figure out if everything in the player spawning region belongs somewhere else. + #region Player spawning helpers + + /// + /// Spawns in a player's mob according to their job and character information at the given coordinates. + /// Used by systems that need to handle spawning players. + /// + /// Coordinates to spawn the character at. + /// Job to assign to the character, if any. + /// Appearance profile to use for the character. + /// The spawned entity + public EntityUid SpawnPlayerMob(EntityCoordinates coordinates, Job? job, HumanoidCharacterProfile? profile) + { + var entity = EntityManager.SpawnEntity( + _prototypeManager.Index(profile?.Species ?? SpeciesManager.DefaultSpecies).Prototype, + coordinates); + + if (job?.StartingGear != null) + { + var startingGear = _prototypeManager.Index(job.StartingGear); + EquipStartingGear(entity, startingGear, profile); + if (profile != null) + EquipIdCard(entity, profile.Name, job.Prototype); + } + + if (profile != null) + { + _humanoidAppearanceSystem.UpdateFromProfile(entity, profile); + EntityManager.GetComponent(entity).EntityName = profile.Name; + } + + foreach (var jobSpecial in job?.Prototype.Special ?? Array.Empty()) + { + jobSpecial.AfterEquip(entity); + } + + return entity; + } + + /// + /// Equips starting gear onto the given entity. + /// + /// Entity to load out. + /// Starting gear to use. + /// Character profile to use, if any. + public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile) + { + if (_inventorySystem.TryGetSlots(entity, out var slotDefinitions)) + { + foreach (var slot in slotDefinitions) + { + var equipmentStr = startingGear.GetGear(slot.Name, profile); + if (!string.IsNullOrEmpty(equipmentStr)) + { + var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent(entity).Coordinates); + _inventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true); + } + } + } + + if (!TryComp(entity, out HandsComponent? handsComponent)) + return; + + var inhand = startingGear.Inhand; + var coords = EntityManager.GetComponent(entity).Coordinates; + foreach (var (hand, prototype) in inhand) + { + var inhandEntity = EntityManager.SpawnEntity(prototype, coords); + _handsSystem.TryPickup(entity, inhandEntity, hand, checkActionBlocker: false, handsComp: handsComponent); + } + } + + /// + /// Equips an ID card and PDA onto the given entity. + /// + /// Entity to load out. + /// Character name to use for the ID. + /// Job prototype to use for the PDA and ID. + public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype) + { + if (!_inventorySystem.TryGetSlotEntity(entity, "id", out var idUid)) + return; + + if (!EntityManager.TryGetComponent(idUid, out PDAComponent? pdaComponent) || pdaComponent.ContainedID == null) + return; + + var card = pdaComponent.ContainedID; + _cardSystem.TryChangeFullName(card.Owner, characterName, card); + _cardSystem.TryChangeJobTitle(card.Owner, jobPrototype.Name, card); + + var access = EntityManager.GetComponent(card.Owner); + var accessTags = access.Tags; + accessTags.UnionWith(jobPrototype.Access); + _pdaSystem.SetOwner(pdaComponent, characterName); + } + + + #endregion Player spawning helpers +} + +/// +/// Ordered broadcast event fired on any spawner eligible to attempt to spawn a player. +/// This event's success is measured by if SpawnResult is not null. +/// You should not make this event's success rely on random chance. +/// This event is designed to use ordered handling. You probably want SpawnPointSystem to be the last handler. +/// +[PublicAPI] +public sealed class PlayerSpawningEvent : EntityEventArgs +{ + /// + /// The entity spawned, if any. You should set this if you succeed at spawning the character, and leave it alone if it's not null. + /// + public EntityUid? SpawnResult; + /// + /// The job to use, if any. + /// + public readonly Job? Job; + /// + /// The profile to use, if any. + /// + public readonly HumanoidCharacterProfile? HumanoidCharacterProfile; + /// + /// The target station, if any. + /// + public readonly EntityUid? Station; + + public PlayerSpawningEvent(Job? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station) + { + Job = job; + HumanoidCharacterProfile = humanoidCharacterProfile; + Station = station; + } +} diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/StationSystem.cs new file mode 100644 index 0000000000..a19753c2e6 --- /dev/null +++ b/Content.Server/Station/Systems/StationSystem.cs @@ -0,0 +1,407 @@ +using System.Linq; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking; +using Content.Server.Maps; +using Content.Server.Station.Components; +using Content.Shared.CCVar; +using JetBrains.Annotations; +using Robust.Shared.Configuration; +using Robust.Shared.Map; +using Robust.Shared.Random; + +namespace Content.Server.Station.Systems; + +/// +/// System that manages stations. +/// A station is, by default, just a name, optional map prototype, and optional grids. +/// For jobs, look at StationJobSystem. For spawning, look at StationSpawningSystem. +/// +[PublicAPI] +public sealed class StationSystem : EntitySystem +{ + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly IGameMapManager _gameMapManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + + private ISawmill _sawmill = default!; + + private readonly HashSet _stations = new(); + + /// + /// All stations that currently exist. + /// + /// + /// I'd have this just invoke an entity query, but I want this to be a hashset for convenience and it allocating on use would be lame. + /// + public IReadOnlySet Stations => _stations; + + private bool _randomStationOffset; + private bool _randomStationRotation; + private float _maxRandomStationOffset; + + /// + public override void Initialize() + { + _sawmill = _logManager.GetSawmill("station"); + + SubscribeLocalEvent(OnRoundEnd); + SubscribeLocalEvent(OnPreGameMapLoad); + SubscribeLocalEvent(OnPostGameMapLoad); + SubscribeLocalEvent(OnStationStartup); + SubscribeLocalEvent(OnStationDeleted); + + _configurationManager.OnValueChanged(CCVars.StationOffset, x => _randomStationOffset = x, true); + _configurationManager.OnValueChanged(CCVars.MaxStationOffset, x => _maxRandomStationOffset = x, true); + _configurationManager.OnValueChanged(CCVars.StationRotation, x => _randomStationRotation = x, true); + } + + #region Event handlers + + private void OnStationStartup(EntityUid uid, StationDataComponent component, ComponentAdd args) + { + _stations.Add(uid); + } + + private void OnStationDeleted(EntityUid uid, StationDataComponent component, ComponentShutdown args) + { + _stations.Remove(uid); + } + + private void OnPreGameMapLoad(PreGameMapLoad ev) + { + // this is only for maps loaded during round setup! + if (_gameTicker.RunLevel == GameRunLevel.InRound) + return; + + if (_randomStationOffset) + ev.Options.Offset += _random.NextVector2(_maxRandomStationOffset); + + if (_randomStationRotation) + ev.Options.Rotation = _random.NextAngle(); + } + + private void OnPostGameMapLoad(PostGameMapLoad ev) + { + var dict = new Dictionary>(); + + void AddGrid(string station, GridId grid) + { + if (dict.ContainsKey(station)) + { + dict[station].Add(grid); + } + else + { + dict[station] = new List {grid}; + } + } + + // Iterate over all BecomesStation + foreach (var grid in ev.Grids) + { + // We still setup the grid + if (!TryComp(_mapManager.GetGridEuid(grid), out var becomesStation)) + continue; + + AddGrid(becomesStation.Id, grid); + } + + if (!dict.Any()) + { + // Oh jeez, no stations got loaded. + // We'll just take the first grid and setup that, then. + + var grid = ev.Grids[0]; + + AddGrid("Station", grid); + } + + // Iterate over all PartOfStation + foreach (var grid in ev.Grids) + { + if (!TryComp(_mapManager.GetGridEuid(grid), out var partOfStation)) + continue; + + AddGrid(partOfStation.Id, grid); + } + + foreach (var (id, gridIds) in dict) + { + StationConfig? stationConfig = null; + if (ev.GameMap.Stations.ContainsKey(id)) + stationConfig = ev.GameMap.Stations[id]; + else + _sawmill.Error($"The station {id} in map {ev.GameMap.ID} does not have an associated station config!"); + InitializeNewStation(stationConfig, gridIds.Select(x => _mapManager.GetGridEuid(x)), ev.StationName); + } + } + + private void OnRoundEnd(GameRunLevelChangedEvent eventArgs) + { + if (eventArgs.New != GameRunLevel.PreRoundLobby) return; + + foreach (var entity in _stations) + { + Del(entity); + } + } + + #endregion Event handlers + + + /// + /// Generates a station name from the given config. + /// + /// + /// + public static string GenerateStationName(StationConfig config) + { + return config.NameGenerator is not null + ? config.NameGenerator.FormatName(config.StationNameTemplate) + : config.StationNameTemplate; + } + + /// + /// Initializes a new station with the given information. + /// + /// The game map prototype used, if any. + /// All grids that should be added to the station. + /// Optional override for the station name. + /// The initialized station. + public EntityUid InitializeNewStation(StationConfig? stationConfig, IEnumerable? gridIds, string? name = null) + { + //HACK: This needs to go in null-space but that crashes currently. + var station = Spawn(null, new MapCoordinates(0, 0, _gameTicker.DefaultMap)); + var data = AddComp(station); + var metaData = MetaData(station); + data.StationConfig = stationConfig; + + if (stationConfig is not null && name is null) + { + metaData.EntityName = GenerateStationName(stationConfig); + } + else if (name is not null) + { + metaData.EntityName = name; + } + else + { + _sawmill.Error($"When setting up station {station}, was unable to find a valid name in the config and no name was provided."); + metaData.EntityName = "unnamed station"; + } + + RaiseLocalEvent(new StationInitializedEvent(station)); + _sawmill.Info($"Set up station {metaData.EntityName} ({station})."); + + foreach (var grid in gridIds ?? Array.Empty()) + { + AddGridToStation(station, grid, null, data); + } + + return station; + } + + /// + /// Adds the given grid to a station. + /// + /// Grid to attach. + /// Station to attach the grid to. + /// Resolve pattern, grid component of mapGrid. + /// Resolve pattern, station data component of station. + /// Thrown when mapGrid or station are not a grid or station, respectively. + public void AddGridToStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null) + { + if (!Resolve(mapGrid, ref gridComponent)) + throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid)); + if (!Resolve(station, ref stationData)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + var stationMember = AddComp(mapGrid); + stationMember.Station = station; + stationData.Grids.Add(gridComponent.GridIndex); + + RaiseLocalEvent(station, new StationGridAddedEvent(gridComponent.GridIndex, false)); + + _sawmill.Info($"Adding grid {mapGrid}:{gridComponent.GridIndex} to station {Name(station)} ({station})"); + } + + /// + /// Removes the given grid from a station. + /// + /// Station to remove the grid from. + /// Grid to remove + /// Resolve pattern, grid component of mapGrid. + /// Resolve pattern, station data component of station. + /// Thrown when mapGrid or station are not a grid or station, respectively. + public void RemoveGridFromStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null) + { + if (!Resolve(mapGrid, ref gridComponent)) + throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid)); + if (!Resolve(station, ref stationData)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + RemComp(mapGrid); + stationData.Grids.Remove(gridComponent.GridIndex); + + RaiseLocalEvent(station, new StationGridRemovedEvent(gridComponent.GridIndex)); + _sawmill.Info($"Removing grid {mapGrid}:{gridComponent.GridIndex} from station {Name(station)} ({station})"); + } + + /// + /// Renames the given station. + /// + /// Station to rename. + /// The new name to apply. + /// Whether or not to announce the rename. + /// Resolve pattern, station data component of station. + /// Resolve pattern, metadata component of station. + /// Thrown when the given station is not a station. + public void RenameStation(EntityUid station, string name, bool loud = true, StationDataComponent? stationData = null, MetaDataComponent? metaData = null) + { + if (!Resolve(station, ref stationData, ref metaData)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + var oldName = metaData.EntityName; + metaData.EntityName = name; + + if (loud) + { + _chatManager.DispatchStationAnnouncement($"The station {oldName} has been renamed to {name}."); + } + + RaiseLocalEvent(station, new StationRenamedEvent(oldName, name)); + } + + /// + /// Deletes the given station. + /// + /// Station to delete. + /// Resolve pattern, station data component of station. + /// Thrown when the given station is not a station. + public void DeleteStation(EntityUid station, StationDataComponent? stationData = null) + { + if (!Resolve(station, ref stationData)) + throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station)); + + _stations.Remove(station); + Del(station); + } + + /// + /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to) + /// + /// Entity to find the owner of. + /// Resolve pattern, transform of the entity. + /// The owning station, if any. + /// + /// This does not remember what station an entity started on, it simply checks where it is currently located. + /// + public EntityUid? GetOwningStation(EntityUid entity, TransformComponent? xform = null) + { + if (!Resolve(entity, ref xform)) + throw new ArgumentException("Tried to use an abstract entity!", nameof(entity)); + + if (TryComp(entity, out _)) + { + // We are the station, just check ourselves. + return CompOrNull(entity)?.Station; + } + + if (xform.GridID == GridId.Invalid) + { + Logger.Debug("A"); + return null; + } + + var grid = _mapManager.GetGridEuid(xform.GridID); + + return CompOrNull(grid)?.Station; + } +} + +/// +/// Broadcast event fired when a station is first set up. +/// This is the ideal point to add components to it. +/// +[PublicAPI] +public sealed class StationInitializedEvent : EntityEventArgs +{ + /// + /// Station this event is for. + /// + public EntityUid Station; + + public StationInitializedEvent(EntityUid station) + { + Station = station; + } +} + +/// +/// Directed event fired on a station when a grid becomes a member of the station. +/// +[PublicAPI] +public sealed class StationGridAddedEvent : EntityEventArgs +{ + /// + /// ID of the grid added to the station. + /// + public GridId GridId; + + /// + /// Indicates that the event was fired during station setup, + /// so that it can be ignored if StationInitializedEvent was already handled. + /// + public bool IsSetup; + + public StationGridAddedEvent(GridId gridId, bool isSetup) + { + GridId = gridId; + IsSetup = isSetup; + } +} + +/// +/// Directed event fired on a station when a grid is no longer a member of the station. +/// +[PublicAPI] +public sealed class StationGridRemovedEvent : EntityEventArgs +{ + /// + /// ID of the grid removed from the station. + /// + public GridId GridId; + + public StationGridRemovedEvent(GridId gridId) + { + GridId = gridId; + } +} + +/// +/// Directed event fired on a station when it is renamed. +/// +[PublicAPI] +public sealed class StationRenamedEvent : EntityEventArgs +{ + /// + /// Prior name of the station. + /// + public string OldName; + + /// + /// New name of the station. + /// + public string NewName; + + public StationRenamedEvent(string oldName, string newName) + { + OldName = oldName; + NewName = newName; + } +} + diff --git a/Content.Server/StationEvents/Events/BureaucraticError.cs b/Content.Server/StationEvents/Events/BureaucraticError.cs index ce5a54e68c..525bab2aca 100644 --- a/Content.Server/StationEvents/Events/BureaucraticError.cs +++ b/Content.Server/StationEvents/Events/BureaucraticError.cs @@ -1,8 +1,6 @@ using System.Linq; -using Content.Server.Station; -using Content.Shared.Roles; +using Content.Server.Station.Systems; using JetBrains.Annotations; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.StationEvents.Events; @@ -11,8 +9,7 @@ namespace Content.Server.StationEvents.Events; public sealed class BureaucraticError : StationEvent { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - public override string? StartAnnouncement => + public override string StartAnnouncement => Loc.GetString("station-event-bureaucratic-error-announcement"); public override string Name => "BureaucraticError"; @@ -27,20 +24,22 @@ public sealed class BureaucraticError : StationEvent public override void Startup() { base.Startup(); - var chosenStation = _random.Pick(EntitySystem.Get().StationInfo.Values.ToList()); - var jobList = chosenStation.JobList.Keys.Where(x => !_prototypeManager.Index(x).IsHead).ToList(); + var stationSystem = EntitySystem.Get(); + var stationJobsSystem = EntitySystem.Get(); + var chosenStation = _random.Pick(stationSystem.Stations.ToList()); + var jobList = stationJobsSystem.GetJobs(chosenStation).Keys.ToList(); // Low chance to completely change up the late-join landscape by closing all positions except infinite slots. // Lower chance than the /tg/ equivalent of this event. if (_random.Prob(0.25f)) { var chosenJob = _random.PickAndTake(jobList); - chosenStation.AdjustJobAmount(chosenJob, -1); // INFINITE chaos. + stationJobsSystem.MakeJobUnlimited(chosenStation, chosenJob); // INFINITE chaos. foreach (var job in jobList) { - if (chosenStation.JobList[job] == -1) + if (stationJobsSystem.IsJobUnlimited(chosenStation, job)) continue; - chosenStation.AdjustJobAmount(job, 0); + stationJobsSystem.TrySetJobSlot(chosenStation, job, 0); } } else @@ -49,11 +48,10 @@ public sealed class BureaucraticError : StationEvent for (var i = 0; i < _random.Next((int)(jobList.Count * 0.20), (int)(jobList.Count * 0.30)); i++) { var chosenJob = _random.PickAndTake(jobList); - if (chosenStation.JobList[chosenJob] == -1) + if (stationJobsSystem.IsJobUnlimited(chosenStation, chosenJob)) continue; - var adj = Math.Max(chosenStation.JobList[chosenJob] + _random.Next(-3, 6), 0); - chosenStation.AdjustJobAmount(chosenJob, adj); + stationJobsSystem.TryAdjustJobSlot(chosenStation, chosenJob, _random.Next(-3, 6)); } } } diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs index 1b3881f3bf..2fdf6e2360 100644 --- a/Content.Server/StationEvents/Events/GasLeak.cs +++ b/Content.Server/StationEvents/Events/GasLeak.cs @@ -1,13 +1,8 @@ using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos; using Content.Shared.Sound; -using Content.Shared.Station; using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Player; using Robust.Shared.Random; @@ -60,7 +55,7 @@ namespace Content.Server.StationEvents.Events // Event variables - private StationId _targetStation; + private EntityUid _targetStation; private EntityUid _targetGrid; diff --git a/Content.Server/StationEvents/Events/RadiationStorm.cs b/Content.Server/StationEvents/Events/RadiationStorm.cs index 3bc0058e33..4126d9918e 100644 --- a/Content.Server/StationEvents/Events/RadiationStorm.cs +++ b/Content.Server/StationEvents/Events/RadiationStorm.cs @@ -1,16 +1,12 @@ using System.Linq; using Content.Server.Radiation; -using Content.Server.Station; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; using Content.Shared.Coordinates; using Content.Shared.Sound; -using Content.Shared.Station; using JetBrains.Annotations; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.StationEvents.Events { @@ -20,8 +16,11 @@ namespace Content.Server.StationEvents.Events // Based on Goonstation style radiation storm with some TG elements (announcer, etc.) [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; + private StationSystem _stationSystem = default!; + public override string Name => "RadiationStorm"; public override string StartAnnouncement => Loc.GetString("station-event-radiation-storm-start-announcement"); protected override string EndAnnouncement => Loc.GetString("station-event-radiation-storm-end-announcement"); @@ -32,7 +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 EntityUid _target = EntityUid.Invalid; private void ResetTimeUntilPulse() { @@ -47,14 +46,17 @@ namespace Content.Server.StationEvents.Events public override void Startup() { + _entityManager.EntitySysManager.Resolve(ref _stationSystem); ResetTimeUntilPulse(); - _target = _robustRandom.Pick(_entityManager.EntityQuery().ToArray()).Station; - base.Startup(); - } - public override void Shutdown() - { - base.Shutdown(); + if (_stationSystem.Stations.Count == 0) + { + Running = false; + return; + } + + _target = _robustRandom.Pick(_stationSystem.Stations); + base.Startup(); } public override void Update(float frameTime) @@ -62,6 +64,11 @@ namespace Content.Server.StationEvents.Events base.Update(frameTime); if (!Started || !Running) return; + if (_target.Valid == false) + { + Running = false; + return; + } _timeUntilPulse -= frameTime; @@ -69,10 +76,14 @@ namespace Content.Server.StationEvents.Events { var mapManager = IoCManager.Resolve(); // Account for split stations by just randomly picking a piece of it. - var possibleTargets = _entityManager.EntityQuery() - .Where(x => x.Station == _target).ToArray(); - StationComponent tempQualifier = _robustRandom.Pick(possibleTargets); - var stationEnt = (tempQualifier).Owner; + var possibleTargets = _entityManager.GetComponent(_target).Grids; + if (possibleTargets.Count == 0) + { + Running = false; + return; + } + + var stationEnt = _robustRandom.Pick(possibleTargets); if (!_entityManager.TryGetComponent(stationEnt, out var grid)) return; diff --git a/Content.Server/StationEvents/Events/StationEvent.cs b/Content.Server/StationEvents/Events/StationEvent.cs index 53d6b71bb5..6ead81b3e2 100644 --- a/Content.Server/StationEvents/Events/StationEvent.cs +++ b/Content.Server/StationEvents/Events/StationEvent.cs @@ -2,15 +2,12 @@ using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Managers; -using Content.Server.Station; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; using Content.Shared.Database; using Content.Shared.Sound; -using Content.Shared.Station; using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Player; using Robust.Shared.Random; @@ -189,20 +186,24 @@ namespace Content.Server.StationEvents.Events } - public static bool TryFindRandomTile(out Vector2i tile, out StationId targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords, IRobustRandom? robustRandom = null, IEntityManager? entityManager = null) + public static bool TryFindRandomTile(out Vector2i tile, out EntityUid targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords, IRobustRandom? robustRandom = null, IEntityManager? entityManager = null, IMapManager? mapManager = null, StationSystem? stationSystem = null) { tile = default; - robustRandom ??= IoCManager.Resolve(); - entityManager ??= IoCManager.Resolve(); + IoCManager.Resolve(ref robustRandom, ref entityManager, ref mapManager); + entityManager.EntitySysManager.Resolve(ref stationSystem); targetCoords = EntityCoordinates.Invalid; - targetStation = robustRandom.Pick(entityManager.EntityQuery().ToArray()).Station; - var t = targetStation; // thanks C# - var possibleTargets = entityManager.EntityQuery() - .Where(x => x.Station == t).ToArray(); - targetGrid = robustRandom.Pick(possibleTargets).Owner; + targetStation = robustRandom.Pick(stationSystem.Stations); + var possibleTargets = entityManager.GetComponent(targetStation).Grids; + if (possibleTargets.Count == 0) + { + targetGrid = EntityUid.Invalid; + return false; + } - if (!entityManager.TryGetComponent(targetGrid!, out var gridComp)) + targetGrid = robustRandom.Pick(possibleTargets); + + if (!entityManager.TryGetComponent(targetGrid, out var gridComp)) return false; var grid = gridComp.Grid; diff --git a/Content.Shared/GameTicking/SharedGameTicker.cs b/Content.Shared/GameTicking/SharedGameTicker.cs index bf96374398..b1edb44382 100644 --- a/Content.Shared/GameTicking/SharedGameTicker.cs +++ b/Content.Shared/GameTicking/SharedGameTicker.cs @@ -1,5 +1,4 @@ -using Content.Shared.Station; -using Robust.Shared.Network; +using Robust.Shared.Network; using Robust.Shared.Serialization; namespace Content.Shared.GameTicking @@ -8,6 +7,7 @@ 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. + //TODO: Move these, they really belong in StationJobsSystem or a cvar. public const string FallbackOverflowJob = "Passenger"; public const string FallbackOverflowJobName = "passenger"; } @@ -108,10 +108,10 @@ namespace Content.Shared.GameTicking /// /// The Status of the Player in the lobby (ready, observer, ...) /// - public Dictionary> JobsAvailableByStation { get; } - public Dictionary StationNames { get; } + public Dictionary> JobsAvailableByStation { get; } + public Dictionary StationNames { get; } - public TickerJobsAvailableEvent(Dictionary stationNames, Dictionary> jobsAvailableByStation) + public TickerJobsAvailableEvent(Dictionary stationNames, Dictionary> jobsAvailableByStation) { StationNames = stationNames; JobsAvailableByStation = jobsAvailableByStation; diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index 6212a94a47..44196b6059 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -46,8 +46,8 @@ namespace Content.Shared.Roles /// Whether this job is a head. /// The job system will try to pick heads before other jobs on the same priority level. /// - [DataField("head")] - public bool IsHead { get; private set; } + [DataField("weight")] + public int Weight { get; private set; } [DataField("startingGear", customTypeSerializer: typeof(PrototypeIdSerializer))] public string? StartingGear { get; private set; } diff --git a/Content.Shared/Station/StationId.cs b/Content.Shared/Station/StationId.cs deleted file mode 100644 index 2e13fa1253..0000000000 --- a/Content.Shared/Station/StationId.cs +++ /dev/null @@ -1,10 +0,0 @@ -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/Resources/Maps/Test/empty.yml b/Resources/Maps/Test/empty.yml index 534b2c9cc2..c14a7dd09c 100644 --- a/Resources/Maps/Test/empty.yml +++ b/Resources/Maps/Test/empty.yml @@ -56,4 +56,10 @@ entities: fixtures: [] bodyType: Dynamic type: Physics +- uid: 1 + type: SpawnPointLatejoin + components: + - parent: 0 + pos: 0,0 + type: Transform ... diff --git a/Resources/Maps/Test/floor3x3.yml b/Resources/Maps/Test/floor3x3.yml index d5ff197658..5dbba1a2e0 100644 --- a/Resources/Maps/Test/floor3x3.yml +++ b/Resources/Maps/Test/floor3x3.yml @@ -125,4 +125,10 @@ entities: type: Gravity - chunkCollection: {} type: DecalGrid +- uid: 1 + type: SpawnPointLatejoin + components: + - parent: 0 + pos: 0,0 + type: Transform ... diff --git a/Resources/Prototypes/Entities/Markers/Spawners/jobs.yml b/Resources/Prototypes/Entities/Markers/Spawners/jobs.yml index aa7230bc93..bbb8497729 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/jobs.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/jobs.yml @@ -14,9 +14,10 @@ - type: entity name: observer spawn point id: SpawnPointObserver - parent: SpawnPointJobBase + parent: MarkerBase components: - type: Sprite + sprite: Markers/jobs.rsi layers: - state: green - texture: Mobs/Ghosts/ghost_human.rsi/icon.png diff --git a/Resources/Prototypes/Maps/bagel.yml b/Resources/Prototypes/Maps/bagel.yml index 7c9331cbbd..68f9c20e4c 100644 --- a/Resources/Prototypes/Maps/bagel.yml +++ b/Resources/Prototypes/Maps/bagel.yml @@ -1,43 +1,45 @@ - type: gameMap id: bagelstation mapName: 'Bagel Station' - mapNameTemplate: '{0} Bagel Station {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/bagel.yml minPlayers: 35 - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 3, 3 ] - Passenger: [ -1, -1 ] - Bartender: [ 2, 2 ] - Botanist: [ 3, 3] - Chef: [ 2, 2 ] - Clown: [ 1, 1 ] - Janitor: [ 3, 3 ] - Mime: [ 1, 1 ] - Captain: [ 1, 1 ] - HeadOfPersonnel: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - StationEngineer: [ 4, 4 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 3, 3 ] - Chemist: [ 2, 3 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 4, 4 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 4, 4 ] - Chaplain: [ 1, 1 ] - Warden: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 2, 2 ] - Quartermaster: [ 1, 1 ] - SalvageSpecialist: [ 3, 3 ] - Musician: [ 1, 1 ] - AtmosphericTechnician: [ 3, 3 ] - TechnicalAssistant: [ 2, 2 ] - MedicalIntern: [ 2, 2 ] - ServiceWorker: [ 2, 2 ] - SecurityCadet: [ 2, 2 ] \ No newline at end of file + stations: + Dart: #TODO: Mapper, fix this name in your map file. (just change the name for every grid with a BecomesStation or PartOfStation component.) + mapNameTemplate: '{0} Bagel Station {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 3, 3 ] + Passenger: [ -1, -1 ] + Bartender: [ 2, 2 ] + Botanist: [ 3, 3 ] + Chef: [ 2, 2 ] + Clown: [ 1, 1 ] + Janitor: [ 3, 3 ] + Mime: [ 1, 1 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 4, 4 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 3, 3 ] + Chemist: [ 2, 3 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 4, 4 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 4, 4 ] + Chaplain: [ 1, 1 ] + Warden: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 2, 2 ] + Quartermaster: [ 1, 1 ] + SalvageSpecialist: [ 3, 3 ] + Musician: [ 1, 1 ] + AtmosphericTechnician: [ 3, 3 ] + TechnicalAssistant: [ 2, 2 ] + MedicalIntern: [ 2, 2 ] + ServiceWorker: [ 2, 2 ] + SecurityCadet: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/delta.yml b/Resources/Prototypes/Maps/delta.yml index 89858e5582..ba1839d224 100644 --- a/Resources/Prototypes/Maps/delta.yml +++ b/Resources/Prototypes/Maps/delta.yml @@ -1,39 +1,41 @@ - type: gameMap id: delta mapName: 'Delta Station' - mapNameTemplate: '{0} Delta Station {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/delta.yml minPlayers: 60 - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 4, 4 ] - Passenger: [ -1, -1 ] - Bartender: [ 1, 1 ] - Botanist: [ 4, 4] - Chef: [ 1, 2 ] - Clown: [ 1, 1 ] - Janitor: [ 1, 2 ] - Mime: [ 1, 1 ] - Captain: [ 1, 1 ] - HeadOfPersonnel: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - StationEngineer: [ 7, 7 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 5, 5 ] - Chemist: [ 2, 2 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 7, 7 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 5, 5 ] - Chaplain: [ 1, 1 ] - Warden: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 2, 2 ] - Quartermaster: [ 1, 1 ] - SalvageSpecialist: [ 3, 3 ] - Musician: [ 2, 2 ] - AtmosphericTechnician: [ 1, 2 ] + stations: + Station: #TODO: Mapper, add a BecomesStation component to the primary grid of the map. + mapNameTemplate: '{0} Delta Station {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 4, 4 ] + Passenger: [ -1, -1 ] + Bartender: [ 1, 1 ] + Botanist: [ 4, 4 ] + Chef: [ 1, 2 ] + Clown: [ 1, 1 ] + Janitor: [ 1, 2 ] + Mime: [ 1, 1 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 7, 7 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 5, 5 ] + Chemist: [ 2, 2 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 7, 7 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 5, 5 ] + Chaplain: [ 1, 1 ] + Warden: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 2, 2 ] + Quartermaster: [ 1, 1 ] + SalvageSpecialist: [ 3, 3 ] + Musician: [ 2, 2 ] + AtmosphericTechnician: [ 1, 2 ] diff --git a/Resources/Prototypes/Maps/game.yml b/Resources/Prototypes/Maps/game.yml index 1a39e338a3..5ae22348b9 100644 --- a/Resources/Prototypes/Maps/game.yml +++ b/Resources/Prototypes/Maps/game.yml @@ -1,185 +1,108 @@ -- type: gameMap - id: saltern - mapName: 'Saltern' - mapNameTemplate: '{0} Saltern {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' - mapPath: /Maps/saltern.yml - minPlayers: 0 - maxPlayers: 29 - fallback: true - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 1, 2 ] - Passenger: [ -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 ] - Chaplain: [ 1, 1 ] - Librarian: [ 1, 1 ] - Musician: [ 1, 1 ] - Lawyer: [ 1, 1 ] - SalvageSpecialist: [ 1, 2 ] - Quartermaster: [ 1, 1 ] - AtmosphericTechnician: [ 1, 2 ] - -- type: gameMap - id: packedstation - mapName: 'Packedstation' - mapNameTemplate: '{0} Packedstation {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: 'VG' - mapPath: /Maps/packedstation.yml - minPlayers: 15 - maxPlayers: 55 - votable: false - conditions: - - !type:HolidayMapCondition - inverted: true - holidays: [ "Christmas" ] - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 2, 3 ] - Passenger: [ -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 ] - Warden: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 1, 2 ] - Musician: [1, 1] - AtmosphericTechnician: [ 1, 2 ] - - type: gameMap id: NSSPillar mapName: 'NSS Pillar' - mapNameTemplate: '{0} NSSPillar {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/nss_pillar.yml minPlayers: 40 - overflowJobs: - - Passenger - availableJobs: - Passenger: [ -1, -1 ] - CargoTechnician: [ 4, 6 ] - Bartender: [ 2, 2 ] - Botanist: [ 2, 4 ] - Chef: [ 2, 4 ] - Clown: [ 1, 2 ] - Janitor: [ 2, 4 ] - Mime: [ 1, 2 ] - Captain: [ 1, 1 ] - HeadOfPersonnel: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - StationEngineer: [ 6, 10 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 6, 10 ] - Chemist: [ 2, 3 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 3, 8 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 6, 10 ] - Chaplain: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 1, 2 ] - Warden: [ 1, 1 ] - Quartermaster: [ 1, 1 ] - SalvageSpecialist: [ 4, 6 ] - Musician: [1, 1] - AtmosphericTechnician: [ 1, 2 ] + stations: + Pillar: + mapNameTemplate: '{0} NSSPillar {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + Passenger: [ -1, -1 ] + CargoTechnician: [ 4, 6 ] + Bartender: [ 2, 2 ] + Botanist: [ 2, 4 ] + Chef: [ 2, 4 ] + Clown: [ 1, 2 ] + Janitor: [ 2, 4 ] + Mime: [ 1, 2 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 6, 10 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 6, 10 ] + Chemist: [ 2, 3 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 3, 8 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 6, 10 ] + Chaplain: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 1, 2 ] + Warden: [ 1, 1 ] + Quartermaster: [ 1, 1 ] + SalvageSpecialist: [ 4, 6 ] + Musician: [ 1, 1 ] + AtmosphericTechnician: [ 1, 2 ] - type: gameMap id: ssreach mapName: 'Reach' - mapNameTemplate: '{0} Reach {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/ssreach.yml minPlayers: 0 maxPlayers: 15 votable: false - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 1, 1 ] - Passenger: [ 4, 8 ] - Bartender: [ 1, 1 ] - Botanist: [ 1, 1 ] - Chef: [ 1, 1 ] - Captain: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - StationEngineer: [ 1, 2 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 1, 1 ] - Chemist: [ 1, 1 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 2, 4 ] - Janitor: [ 1, 1 ] - Musician: [1, 1] + stations: + Reach: + mapNameTemplate: '{0} Reach {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 1, 1 ] + Passenger: [ 4, 8 ] + Bartender: [ 1, 1 ] + Botanist: [ 1, 1 ] + Chef: [ 1, 1 ] + Captain: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 1, 2 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 1, 1 ] + Chemist: [ 1, 1 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 2, 4 ] + Janitor: [ 1, 1 ] + Musician: [1, 1] - type: gameMap id: dart mapName: 'Dart' - mapNameTemplate: '{0} Dart {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/dart.yml minPlayers: 0 votable: false - overflowJobs: [] - availableJobs: - Captain: [ 1, 1 ] + stations: + Station: #TODO: Mapper, add a BecomesStation component to the primary grid of the map. + mapNameTemplate: '{0} Dart {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: [] + availableJobs: + Captain: [ 1, 1 ] - type: gameMap id: moonrise mapName: 'Moonrise ERC' - mapNameTemplate: '{0} Moonrise {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: 'VG' mapPath: /Maps/moonrise.yml minPlayers: 0 votable: false - overflowJobs: [] - availableJobs: - Captain: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - ChiefMedicalOfficer: [ 1, 1 ] - SecurityOfficer: [ 3, 6 ] + stations: + Station: + mapNameTemplate: '{0} Moonrise {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: 'VG' + overflowJobs: [] + availableJobs: + Captain: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + ChiefMedicalOfficer: [ 1, 1 ] + SecurityOfficer: [ 3, 6 ] diff --git a/Resources/Prototypes/Maps/holiday.yml b/Resources/Prototypes/Maps/holiday.yml index 8c06d241c1..84bd64f241 100644 --- a/Resources/Prototypes/Maps/holiday.yml +++ b/Resources/Prototypes/Maps/holiday.yml @@ -1,36 +1,36 @@ -- type: gameMap - id: packedstationxmas - mapName: 'Packedmasstation' - mapNameTemplate: '{0} Packedmasstation {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: 'VG' - mapPath: /Maps/packedstationxmas.yml - minPlayers: 15 - conditions: - - !type:HolidayMapCondition - holidays: [ "Christmas" ] - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 2, 3 ] - Passenger: [ -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 ] - Warden: [ 1, 1 ] +#- type: gameMap +# id: packedstationxmas +# mapName: 'Packedmasstation' +# mapNameTemplate: '{0} Packedmasstation {1}' +# nameGenerator: +# !type:NanotrasenNameGenerator +# prefixCreator: 'VG' +# mapPath: /Maps/packedstationxmas.yml +# minPlayers: 15 +# conditions: +# - !type:HolidayMapCondition +# holidays: [ "Christmas" ] +# overflowJobs: +# - Passenger +# availableJobs: +# CargoTechnician: [ 2, 3 ] +# Passenger: [ -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 ] +# Warden: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/marathon.yml b/Resources/Prototypes/Maps/marathon.yml index acf9f3717b..77c58d276d 100644 --- a/Resources/Prototypes/Maps/marathon.yml +++ b/Resources/Prototypes/Maps/marathon.yml @@ -1,44 +1,46 @@ - type: gameMap id: marathonstation mapName: 'Marathon Station' - mapNameTemplate: '{0} Marathon Station {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/marathon.yml minPlayers: 35 maxPlayers: 70 - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 2, 3 ] - Passenger: [ -1, -1 ] - Bartender: [ 2, 2 ] - Botanist: [ 3, 3] - Chef: [ 2, 2 ] - Clown: [ 1, 1 ] - Janitor: [ 1, 2 ] - Mime: [ 1, 1 ] - Captain: [ 1, 1 ] - HeadOfPersonnel: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - StationEngineer: [ 4, 4 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 3, 4 ] - Chemist: [ 2, 3 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 3, 4 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 4, 4 ] - Chaplain: [ 1, 1 ] - Warden: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 2, 2 ] - Quartermaster: [ 1, 1 ] - SalvageSpecialist: [ 3, 3 ] - Musician: [ 1, 1 ] - AtmosphericTechnician: [ 3, 3 ] - TechnicalAssistant: [ 2, 2 ] - MedicalIntern: [ 2, 2 ] - ServiceWorker: [ 2, 2 ] - SecurityCadet: [ 2, 2 ] \ No newline at end of file + stations: + Station: #TODO: Mapper, add a BecomesStation component to the primary grid of the map. + mapNameTemplate: '{0} Marathon Station {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 2, 3 ] + Passenger: [ -1, -1 ] + Bartender: [ 2, 2 ] + Botanist: [ 3, 3] + Chef: [ 2, 2 ] + Clown: [ 1, 1 ] + Janitor: [ 1, 2 ] + Mime: [ 1, 1 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 4, 6 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 3, 4 ] + Chemist: [ 2, 3 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 3, 4 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 4, 4 ] + Chaplain: [ 1, 1 ] + Warden: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 2, 2 ] + Quartermaster: [ 1, 1 ] + SalvageSpecialist: [ 3, 3 ] + Musician: [ 1, 1 ] + AtmosphericTechnician: [ 3, 3 ] + TechnicalAssistant: [ 2, 2 ] + MedicalIntern: [ 2, 2 ] + ServiceWorker: [ 2, 2 ] + SecurityCadet: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/moose.yml b/Resources/Prototypes/Maps/moose.yml index 879a3f43f2..8b96bb401c 100644 --- a/Resources/Prototypes/Maps/moose.yml +++ b/Resources/Prototypes/Maps/moose.yml @@ -1,44 +1,46 @@ - type: gameMap id: moosestation mapName: 'Moose Station' - mapNameTemplate: '{0} Moose Station {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/moose.yml minPlayers: 0 maxPlayers: 35 - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 2, 2 ] - Passenger: [ -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: [ 3, 3 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 2, 2 ] - Chemist: [ 2, 2 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 3, 3 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 2, 2 ] - Chaplain: [ 1, 1 ] - Warden: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 1, 1 ] - Quartermaster: [ 1, 1 ] - SalvageSpecialist: [ 2, 2 ] - Musician: [ 1, 1 ] - AtmosphericTechnician: [ 2, 2 ] - TechnicalAssistant: [ 1, 1 ] - MedicalIntern: [ 1, 1 ] - ServiceWorker: [ 1, 1 ] - SecurityCadet: [ 1, 1 ] \ No newline at end of file + stations: + Station: #TODO: Mapper, add a BecomesStation component to the primary grid of the map. + mapNameTemplate: '{0} Moose Station {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 2, 2 ] + Passenger: [ -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: [ 3, 3 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 2, 2 ] + Chemist: [ 2, 2 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 3, 3 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 2, 2 ] + Chaplain: [ 1, 1 ] + Warden: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 1, 1 ] + Quartermaster: [ 1, 1 ] + SalvageSpecialist: [ 2, 2 ] + Musician: [ 1, 1 ] + AtmosphericTechnician: [ 2, 2 ] + TechnicalAssistant: [ 1, 1 ] + MedicalIntern: [ 1, 1 ] + ServiceWorker: [ 1, 1 ] + SecurityCadet: [ 1, 1 ] diff --git a/Resources/Prototypes/Maps/packedstation.yml b/Resources/Prototypes/Maps/packedstation.yml new file mode 100644 index 0000000000..187f6b3ab1 --- /dev/null +++ b/Resources/Prototypes/Maps/packedstation.yml @@ -0,0 +1,40 @@ +- type: gameMap + id: packedstation + mapName: 'Packedstation' + mapPath: /Maps/packedstation.yml + minPlayers: 15 + maxPlayers: 55 + votable: false + stations: + Packed: + mapNameTemplate: '{0} Packedstation {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: 'VG' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 2, 3 ] + Passenger: [ -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 ] + Warden: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 1, 2 ] + Musician: [1, 1] diff --git a/Resources/Prototypes/Maps/saltern.yml b/Resources/Prototypes/Maps/saltern.yml index eb14ef775c..e68e7b789f 100644 --- a/Resources/Prototypes/Maps/saltern.yml +++ b/Resources/Prototypes/Maps/saltern.yml @@ -1,44 +1,45 @@ -- type: gameMap - id: saltern - mapName: 'Saltern' - mapNameTemplate: '{0} Saltern {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' - mapPath: /Maps/saltern.yml - minPlayers: 0 - maxPlayers: 29 - fallback: true - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 2, 2 ] - Passenger: [ -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: [ 3, 3 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 2, 3 ] - Chemist: [ 1, 1 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 2, 3 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 3, 3 ] - Chaplain: [ 1, 1 ] - Librarian: [ 1, 1 ] - Musician: [ 1, 1 ] - Lawyer: [ 1, 1 ] - SalvageSpecialist: [ 2, 2 ] - Quartermaster: [ 1, 1 ] - AtmosphericTechnician: [ 2, 2 ] - TechnicalAssistant: [ 2, 2 ] - MedicalIntern: [ 2, 2 ] - ServiceWorker: [ 2, 2 ] - SecurityCadet: [ 2, 2 ] +- type: gameMap + id: saltern + mapName: 'Saltern' + mapPath: /Maps/saltern.yml + minPlayers: 0 + maxPlayers: 29 + fallback: true + stations: + Saltern: + mapNameTemplate: '{0} Saltern {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 1, 2 ] + Passenger: [ -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 ] + Chaplain: [ 1, 1 ] + Librarian: [ 1, 1 ] + Musician: [ 1, 1 ] + Lawyer: [ 1, 1 ] + SalvageSpecialist: [ 1, 2 ] + Quartermaster: [ 1, 1 ] + TechnicalAssistant: [ 2, 2 ] + MedicalIntern: [ 2, 2 ] + ServiceWorker: [ 2, 2 ] + SecurityCadet: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/splitstation.yml b/Resources/Prototypes/Maps/splitstation.yml index 7b3beff9f3..966e1443f7 100644 --- a/Resources/Prototypes/Maps/splitstation.yml +++ b/Resources/Prototypes/Maps/splitstation.yml @@ -1,43 +1,45 @@ - type: gameMap id: splitstation mapName: 'Split Station' - mapNameTemplate: '{0} Split Station {1}' - nameGenerator: - !type:NanotrasenNameGenerator - prefixCreator: '14' mapPath: /Maps/splitstation.yml - minPlayers: 60 - overflowJobs: - - Passenger - availableJobs: - CargoTechnician: [ 3, 6 ] - Passenger: [ -1, -1 ] - Bartender: [ 2, 3 ] - Botanist: [ 3, 6] - Chef: [ 2, 4 ] - Clown: [ 1, 2 ] - Janitor: [ 2, 4 ] - Mime: [ 1, 2 ] - Captain: [ 1, 1 ] - HeadOfPersonnel: [ 1, 1 ] - ChiefEngineer: [ 1, 1 ] - StationEngineer: [ 6, 8 ] - ChiefMedicalOfficer: [ 1, 1 ] - MedicalDoctor: [ 3, 8 ] - Chemist: [ 2, 3 ] - ResearchDirector: [ 1, 1 ] - Scientist: [ 3, 6 ] - HeadOfSecurity: [ 1, 1 ] - SecurityOfficer: [ 8, 10 ] - Chaplain: [ 1, 1 ] - Warden: [ 1, 1 ] - Librarian: [ 1, 1 ] - Lawyer: [ 2, 2 ] - Quartermaster: [ 1, 1 ] - SalvageSpecialist: [ 4, 6 ] - Musician: [ 1, 1 ] - AtmosphericTechnician: [ 1, 2 ] - TechnicalAssistant: [ 2, 2 ] - MedicalIntern: [ 2, 2 ] - ServiceWorker: [ 2, 2 ] - SecurityCadet: [ 2, 2 ] + minPlayers: 40 + stations: + Dart: #TODO: Mapper, fix this name in your map file. (just change the name for every grid with a BecomesStation or PartOfStation component.) + mapNameTemplate: '{0} Split Station {1}' + nameGenerator: + !type:NanotrasenNameGenerator + prefixCreator: '14' + overflowJobs: + - Passenger + availableJobs: + CargoTechnician: [ 3, 6 ] + Passenger: [ -1, -1 ] + Bartender: [ 2, 3 ] + Botanist: [ 3, 6] + Chef: [ 2, 4 ] + Clown: [ 1, 2 ] + Janitor: [ 2, 4 ] + Mime: [ 1, 2 ] + Captain: [ 1, 1 ] + HeadOfPersonnel: [ 1, 1 ] + ChiefEngineer: [ 1, 1 ] + StationEngineer: [ 6, 8 ] + ChiefMedicalOfficer: [ 1, 1 ] + MedicalDoctor: [ 3, 8 ] + Chemist: [ 2, 3 ] + ResearchDirector: [ 1, 1 ] + Scientist: [ 3, 6 ] + HeadOfSecurity: [ 1, 1 ] + SecurityOfficer: [ 8, 10 ] + Chaplain: [ 1, 1 ] + Warden: [ 1, 1 ] + Librarian: [ 1, 1 ] + Lawyer: [ 2, 2 ] + Quartermaster: [ 1, 1 ] + SalvageSpecialist: [ 4, 6 ] + Musician: [ 1, 1 ] + AtmosphericTechnician: [ 1, 2 ] + TechnicalAssistant: [ 2, 2 ] + MedicalIntern: [ 2, 2 ] + ServiceWorker: [ 2, 2 ] + SecurityCadet: [ 2, 2 ] diff --git a/Resources/Prototypes/Maps/test.yml b/Resources/Prototypes/Maps/test.yml index 2e9c01f2f3..9c34a5c95a 100644 --- a/Resources/Prototypes/Maps/test.yml +++ b/Resources/Prototypes/Maps/test.yml @@ -4,8 +4,10 @@ mapPath: /Maps/Test/empty.yml minPlayers: 0 votable: false - overflowJobs: - - Passenger - availableJobs: - Passenger: [ -1, -1 ] - + stations: + Station: #TODO: Add a BecomesStation to empty.yml + mapNameTemplate: "Empty" + overflowJobs: + - Passenger + availableJobs: + Passenger: [ -1, -1 ] diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml index b63d214ffd..f7c83ddfb1 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml @@ -1,13 +1,13 @@ - type: job id: Quartermaster name: "quartermaster" - head: true + weight: 10 startingGear: QuartermasterGear departments: - Cargo icon: "QuarterMaster" supervisors: "the head of personnel" - canBeAntag: false + canBeAntag: false access: - Cargo - Salvage diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index 9a59382af4..84b72788e8 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -1,7 +1,7 @@ - type: job id: Captain name: "captain" - head: true + weight: 20 startingGear: CaptainGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml index e167362640..16f34abd78 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml @@ -1,7 +1,7 @@ - type: job id: HeadOfPersonnel name: "head of personnel" - head: true + weight: 20 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 757c809fbb..623bcb4656 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml @@ -1,7 +1,7 @@ - type: job id: ChiefEngineer name: "chief engineer" - head: true + weight: 10 startingGear: ChiefEngineerGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index 1eca168320..4bf57a9546 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -3,7 +3,7 @@ - type: job id: ChiefMedicalOfficer name: "chief medical officer" - head: true + weight: 10 startingGear: CMOGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index d43030a0f1..9afd94a042 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -1,7 +1,7 @@ - type: job id: ResearchDirector name: "research director" - head: true + weight: 10 startingGear: ResearchDirectorGear departments: - Command diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml index ae681f785a..46f93b2e10 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml @@ -1,7 +1,7 @@ - type: job id: HeadOfSecurity name: "head of security" - head: true + weight: 10 startingGear: HoSGear departments: - Command diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index edd35665c8..936e3d7481 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -201,6 +201,7 @@ True True True + True True True True @@ -305,5 +306,6 @@ True True True + True True True