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 <lunarautomaton6@gmail.com> * 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 <lunarautomaton6@gmail.com> * 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 <lunarautomaton6@gmail.com> * 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 <lunarautomaton6@gmail.com>
This commit is contained in:
@@ -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<StationId, Dictionary<string, int>> _jobsAvailable = new();
|
||||
private Dictionary<StationId, string> _stationNames = new();
|
||||
private Dictionary<EntityUid, Dictionary<string, uint?>> _jobsAvailable = new();
|
||||
private Dictionary<EntityUid, string> _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<NetUserId, LobbyPlayerStatus> Status { get; private set; } = new();
|
||||
[ViewVariables] public IReadOnlyDictionary<StationId, Dictionary<string, int>> JobsAvailable => _jobsAvailable;
|
||||
[ViewVariables] public IReadOnlyDictionary<StationId, string> StationNames => _stationNames;
|
||||
[ViewVariables] public IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>> JobsAvailable => _jobsAvailable;
|
||||
[ViewVariables] public IReadOnlyDictionary<EntityUid, string> StationNames => _stationNames;
|
||||
|
||||
public event Action? InfoBlobUpdated;
|
||||
public event Action? LobbyStatusUpdated;
|
||||
public event Action? LobbyReadyUpdated;
|
||||
public event Action? LobbyLateJoinStatusUpdated;
|
||||
public event Action<IReadOnlyDictionary<StationId, Dictionary<string, int>>>? LobbyJobsAvailableUpdated;
|
||||
public event Action<IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>>>? LobbyJobsAvailableUpdated;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -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<StationId, Dictionary<string, JobButton>> _jobButtons = new();
|
||||
private readonly Dictionary<StationId, Dictionary<string, BoxContainer>> _jobCategories = new();
|
||||
private readonly Dictionary<EntityUid, Dictionary<string, JobButton>> _jobButtons = new();
|
||||
private readonly Dictionary<EntityUid, Dictionary<string, BoxContainer>> _jobCategories = new();
|
||||
private readonly List<ScrollContainer> _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<StationId, Dictionary<string, int>> _)
|
||||
private void JobsAvailableUpdated(IReadOnlyDictionary<EntityUid, Dictionary<string, uint?>> _)
|
||||
{
|
||||
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;
|
||||
|
||||
215
Content.IntegrationTests/Tests/Station/StationJobsTest.cs
Normal file
215
Content.IntegrationTests/Tests/Station/StationJobsTest.cs
Normal file
@@ -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<IPrototypeManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
|
||||
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
|
||||
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
|
||||
var stationSystem = entSysMan.GetEntitySystem<StationSystem>();
|
||||
|
||||
List<EntityUid> 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<NetUserId, HumanoidCharacterProfile>()
|
||||
.AddJob("TAssistant", JobPriority.Medium, PlayerCount)
|
||||
.AddPreference("TClown", JobPriority.Low)
|
||||
.AddPreference("TMime", JobPriority.High)
|
||||
.WithPlayers(
|
||||
new Dictionary<NetUserId, HumanoidCharacterProfile>()
|
||||
.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<IPrototypeManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var fooStationProto = prototypeManager.Index<GameMapPrototype>("FooStation");
|
||||
var entSysMan = server.ResolveDependency<IEntityManager>().EntitySysManager;
|
||||
var stationJobs = entSysMan.GetEntitySystem<StationJobsSystem>();
|
||||
var stationSystem = entSysMan.GetEntitySystem<StationSystem>();
|
||||
|
||||
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<NetUserId, HumanoidCharacterProfile> AddJob(
|
||||
this Dictionary<NetUserId, HumanoidCharacterProfile> 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<NetUserId, HumanoidCharacterProfile> AddPreference(
|
||||
this Dictionary<NetUserId, HumanoidCharacterProfile> inp, string jobId, JobPriority prio = JobPriority.Medium)
|
||||
{
|
||||
return inp.ToDictionary(x => x.Key, x => x.Value.WithJobPriority(jobId, prio));
|
||||
}
|
||||
|
||||
public static Dictionary<NetUserId, HumanoidCharacterProfile> WithPlayers(
|
||||
this Dictionary<NetUserId, HumanoidCharacterProfile> inp,
|
||||
Dictionary<NetUserId, HumanoidCharacterProfile> second)
|
||||
{
|
||||
return new[] {inp, second}.SelectMany(x => x).ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
@@ -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<GameTicker>();
|
||||
var stationSpawning = EntitySystem.Get<StationSpawningSystem>();
|
||||
var protoManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
var startingGear = protoManager.Index<StartingGearPrototype>(StartingGearPrototype);
|
||||
gameTicker.EquipStartingGear(Owner, startingGear, null);
|
||||
stationSpawning.EquipStartingGear(Owner, startingGear, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IPrototypeManager>();
|
||||
var stationSystem = EntitySystem.Get<StationSystem>();
|
||||
var stationJobs = EntitySystem.Get<StationJobsSystem>();
|
||||
|
||||
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<IPrototypeManager>();
|
||||
var station = new EntityUid(stationInt);
|
||||
|
||||
if (!prototypeManager.TryIndex<JobPrototype>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StationSystem>();
|
||||
var stationJobs = EntitySystem.Get<StationJobsSystem>();
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StationSystem>().StationInfo)
|
||||
foreach (var station in EntitySystem.Get<StationSystem>().Stations)
|
||||
{
|
||||
shell.WriteLine($"{id.Id, -2} | {station.Name} | {station.MapPrototype.ID}");
|
||||
var name = _entityManager.GetComponent<MetaDataComponent>(station).EntityName;
|
||||
shell.WriteLine($"{station, -10} | {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StationSystem>();
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GameTicker>();
|
||||
var stationSystem = EntitySystem.Get<StationSystem>();
|
||||
var stationJobs = EntitySystem.Get<StationJobsSystem>();
|
||||
|
||||
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<JobPrototype>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ManifestEntry> _manifest = new();
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<string, int> _spawnedPositions = new();
|
||||
|
||||
private Dictionary<IPlayerSession, (string, StationId)> AssignJobs(List<IPlayerSession> availablePlayers,
|
||||
Dictionary<NetUserId, HumanoidCharacterProfile> profiles)
|
||||
{
|
||||
var assigned = new Dictionary<IPlayerSession, (string, StationId)>();
|
||||
|
||||
List<(IPlayerSession, List<string>)> 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<string, int> 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<JobPrototype>(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<StationId, string>(), new Dictionary<StationId, Dictionary<string, int>>());
|
||||
|
||||
var jobs = new Dictionary<StationId, Dictionary<string, int>>();
|
||||
var stationNames = new Dictionary<StationId, string>();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// Can't yet be removed because every test ever seems to depend on it. I'll make removing this a different PR.
|
||||
/// </summary>
|
||||
[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<EntityCoordinates> _possiblePositions = new();
|
||||
|
||||
private void SpawnPlayers(List<IPlayerSession> readyPlayers, IPlayerSession[] origReadyPlayers,
|
||||
private void SpawnPlayers(List<IPlayerSession> readyPlayers, IEnumerable<NetUserId> origReadyPlayers,
|
||||
Dictionary<NetUserId, HumanoidCharacterProfile> 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<IPlayerSession, (string, StationId)> assignedJobs,
|
||||
IPlayerSession[] origReadyPlayers, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> 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<OwOAccentComponent>(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<SpeciesPrototype>(profile?.Species ?? SpeciesManager.DefaultSpecies).Prototype,
|
||||
coordinates);
|
||||
|
||||
if (job.StartingGear != null)
|
||||
{
|
||||
var startingGear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
|
||||
EquipStartingGear(entity, startingGear, profile);
|
||||
}
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_humanoidAppearanceSystem.UpdateFromProfile(entity, profile);
|
||||
EntityManager.GetComponent<MetaDataComponent>(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<TransformComponent>(entity).Coordinates);
|
||||
_inventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryComp(entity, out HandsComponent? handsComponent))
|
||||
return;
|
||||
|
||||
var inhand = startingGear.Inhand;
|
||||
var coords = EntityManager.GetComponent<TransformComponent>(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<AccessComponent>(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<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
var matchingStation =
|
||||
EntityManager.TryGetComponent<StationComponent>(transform.ParentUid, out var stationComponent) &&
|
||||
stationComponent.Station == station;
|
||||
DebugTools.Assert(EntityManager.TryGetComponent<IMapGridComponent>(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<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
var matchingStation =
|
||||
EntityManager.TryGetComponent<StationComponent>(transform.ParentUid, out var stationComponent) &&
|
||||
stationComponent.Station == station;
|
||||
DebugTools.Assert(EntityManager.TryGetComponent<IMapGridComponent>(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.
|
||||
/// </summary>
|
||||
[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.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
@@ -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<JobPrototype>(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!;
|
||||
|
||||
@@ -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<EntityLootTablePrototype>(SuspicionLootTable);
|
||||
|
||||
foreach (var (_, mapGrid) in EntityManager.EntityQuery<StationComponent, IMapGridComponent>(true))
|
||||
foreach (var (_, mapGrid) in EntityManager.EntityQuery<StationMemberComponent, IMapGridComponent>(true))
|
||||
{
|
||||
// I'm so sorry.
|
||||
var tiles = mapGrid.Grid.GetAllTiles().ToArray();
|
||||
|
||||
@@ -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<string> _previousMaps = new Queue<string>();
|
||||
private readonly Queue<string> _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;
|
||||
|
||||
35
Content.Server/Maps/GameMapPrototype.MapSelection.cs
Normal file
35
Content.Server/Maps/GameMapPrototype.MapSelection.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Content.Server.Maps;
|
||||
|
||||
public sealed partial class GameMapPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls if the map can be used as a fallback if no maps are eligible.
|
||||
/// </summary>
|
||||
[DataField("fallback")]
|
||||
public bool Fallback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls if the map can be voted for.
|
||||
/// </summary>
|
||||
[DataField("votable")]
|
||||
public bool Votable { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum players for the given map.
|
||||
/// </summary>
|
||||
[DataField("minPlayers", required: true)]
|
||||
public uint MinPlayers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum players for the given map.
|
||||
/// </summary>
|
||||
[DataField("maxPlayers")]
|
||||
public uint MaxPlayers { get; } = uint.MaxValue;
|
||||
|
||||
[DataField("conditions")] private readonly List<GameMapCondition> _conditions = new();
|
||||
|
||||
/// <summary>
|
||||
/// The game map conditions that must be fulfilled for this map to be selectable.
|
||||
/// </summary>
|
||||
public IReadOnlyList<GameMapCondition> Conditions => _conditions;
|
||||
}
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// Prototype data for a game map.
|
||||
/// </summary>
|
||||
[Prototype("gameMap")]
|
||||
public sealed class GameMapPrototype : IPrototype
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[Prototype("gameMap"), PublicAPI]
|
||||
public sealed partial class GameMapPrototype : IPrototype
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[IdDataFieldAttribute]
|
||||
[IdDataField]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum players for the given map.
|
||||
/// </summary>
|
||||
[DataField("minPlayers", required: true)]
|
||||
public uint MinPlayers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum players for the given map.
|
||||
/// </summary>
|
||||
[DataField("maxPlayers")]
|
||||
public uint MaxPlayers { get; } = uint.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the map to use in generic messages, like the map vote.
|
||||
/// </summary>
|
||||
@@ -38,49 +26,16 @@ public sealed class GameMapPrototype : IPrototype
|
||||
public string MapName { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the given map.
|
||||
/// </summary>
|
||||
[DataField("mapNameTemplate")]
|
||||
public string? MapNameTemplate { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Name generator to use for the map, if any.
|
||||
/// </summary>
|
||||
[DataField("nameGenerator")]
|
||||
public GameMapNameGenerator? NameGenerator { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Relative directory path to the given map, i.e. `Maps/saltern.yml`
|
||||
/// Relative directory path to the given map, i.e. `/Maps/saltern.yml`
|
||||
/// </summary>
|
||||
[DataField("mapPath", required: true)]
|
||||
public ResourcePath MapPath { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Controls if the map can be used as a fallback if no maps are eligible.
|
||||
/// </summary>
|
||||
[DataField("fallback")]
|
||||
public bool Fallback { get; }
|
||||
[DataField("stations")]
|
||||
private Dictionary<string, StationConfig> _stations = new();
|
||||
|
||||
/// <summary>
|
||||
/// Controls if the map can be voted for.
|
||||
/// The stations this map contains. The names should match with the BecomesStation components.
|
||||
/// </summary>
|
||||
[DataField("votable")]
|
||||
public bool Votable { get; } = true;
|
||||
|
||||
[DataField("conditions")]
|
||||
public List<GameMapCondition> Conditions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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!
|
||||
/// </summary>
|
||||
[DataField("overflowJobs", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<JobPrototype>))]
|
||||
public List<string> OverflowJobs { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Index of all jobs available on the station, of form
|
||||
/// jobname: [roundstart, midround]
|
||||
/// </summary>
|
||||
[DataField("availableJobs", required: true, customTypeSerializer:typeof(PrototypeIdDictionarySerializer<List<int>, JobPrototype>))]
|
||||
public Dictionary<string, List<int>> AvailableJobs { get; } = default!;
|
||||
public IReadOnlyDictionary<string, StationConfig> Stations => _stations;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,4 @@ public interface IGameMapManager
|
||||
/// <param name="gameMap">name of the map</param>
|
||||
/// <returns>existence</returns>
|
||||
bool CheckMapExists(string gameMap);
|
||||
|
||||
public string GenerateMapName(GameMapPrototype gameMap);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Where the map comes from. Should be a two or three letter code, for example "VG" for Packedstation.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
57
Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs
Normal file
57
Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs
Normal file
@@ -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<PlayerSpawningEvent>(OnSpawnPlayer);
|
||||
}
|
||||
|
||||
private void OnSpawnPlayer(PlayerSpawningEvent args)
|
||||
{
|
||||
// TODO: Cache all this if it ends up important.
|
||||
var points = EntityQuery<SpawnPointComponent>().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!");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Added to grids saved in maps to designate that they are the 'main station' grid.
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
28
Content.Server/Station/Components/StationDataComponent.cs
Normal file
28
Content.Server/Station/Components/StationDataComponent.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Station.Systems;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Station.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores core information about a station, namely it's config and associated grids.
|
||||
/// All station entities will have this component.
|
||||
/// </summary>
|
||||
[RegisterComponent, Friend(typeof(StationSystem))]
|
||||
public sealed class StationDataComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The game map prototype, if any, associated with this station.
|
||||
/// </summary>
|
||||
[DataField("stationConfig")]
|
||||
public StationConfig? StationConfig = null;
|
||||
|
||||
/// <summary>
|
||||
/// List of all grids this station is part of.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You should not mutate this yourself, go through StationSystem so the appropriate events get fired.
|
||||
/// </remarks>
|
||||
[DataField("grids")]
|
||||
public readonly HashSet<EntityUid> Grids = new();
|
||||
}
|
||||
61
Content.Server/Station/Components/StationJobsComponent.cs
Normal file
61
Content.Server/Station/Components/StationJobsComponent.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about a station's job selection.
|
||||
/// </summary>
|
||||
[RegisterComponent, Friend(typeof(StationJobsSystem)), PublicAPI]
|
||||
public sealed class StationJobsComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Total *round-start* jobs at station start.
|
||||
/// </summary>
|
||||
[DataField("roundStartTotalJobs")] public int RoundStartTotalJobs;
|
||||
|
||||
/// <summary>
|
||||
/// Total *mid-round* jobs at station start.
|
||||
/// </summary>
|
||||
[DataField("midRoundTotalJobs")] public int MidRoundTotalJobs;
|
||||
|
||||
/// <summary>
|
||||
/// Current total jobs.
|
||||
/// </summary>
|
||||
[DataField("totalJobs")] public int TotalJobs;
|
||||
|
||||
/// <summary>
|
||||
/// The percentage of jobs remaining.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if MidRoundTotalJobs is zero. This is a NaN free API.
|
||||
/// </remarks>
|
||||
[ViewVariables]
|
||||
public float? PercentJobsRemaining => MidRoundTotalJobs > 0 ? TotalJobs / (float) MidRoundTotalJobs : null;
|
||||
|
||||
/// <summary>
|
||||
/// The current list of jobs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should not be mutated or used directly unless you really know what you're doing, go through StationJobsSystem.
|
||||
/// </remarks>
|
||||
[DataField("jobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<uint?, JobPrototype>))]
|
||||
public Dictionary<string, uint?> JobList = new();
|
||||
|
||||
/// <summary>
|
||||
/// The round-start list of jobs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should not be mutated, ever.
|
||||
/// </remarks>
|
||||
[DataField("roundStartJobList", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<uint?, JobPrototype>))]
|
||||
public Dictionary<string, uint?> RoundStartJobList = new();
|
||||
|
||||
/// <summary>
|
||||
/// Overflow jobs that round-start can spawn infinitely many of.
|
||||
/// </summary>
|
||||
[DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))] public HashSet<string> OverflowJobs = new();
|
||||
}
|
||||
16
Content.Server/Station/Components/StationMemberComponent.cs
Normal file
16
Content.Server/Station/Components/StationMemberComponent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Content.Server.Station.Systems;
|
||||
|
||||
namespace Content.Server.Station.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a grid is a member of the given station.
|
||||
/// </summary>
|
||||
[RegisterComponent, Friend(typeof(StationSystem))]
|
||||
public sealed class StationMemberComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Station that this grid is a part of.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid Station = EntityUid.Invalid;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Content.Server.Station.Systems;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Station.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Controls spawning on the given station, tracking spawners present on it.
|
||||
/// </summary>
|
||||
[RegisterComponent, Friend(typeof(StationSpawningSystem))]
|
||||
public sealed class StationSpawningComponent : Component
|
||||
{
|
||||
}
|
||||
28
Content.Server/Station/StationConfig.Jobs.cs
Normal file
28
Content.Server/Station/StationConfig.Jobs.cs
Normal file
@@ -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<JobPrototype>))]
|
||||
private readonly List<string> _overflowJobs = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 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!
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> OverflowJobs => _overflowJobs;
|
||||
|
||||
|
||||
[DataField("availableJobs", required: true,
|
||||
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))]
|
||||
private readonly Dictionary<string, List<int?>> _availableJobs = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Index of all jobs available on the station, of form
|
||||
/// job name: [round-start, mid-round]
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, List<int?>> AvailableJobs => _availableJobs;
|
||||
}
|
||||
33
Content.Server/Station/StationConfig.cs
Normal file
33
Content.Server/Station/StationConfig.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[DataDefinition, PublicAPI]
|
||||
public sealed partial class StationConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The name template to use for the station.
|
||||
/// If there's a name generator this should follow it's required format.
|
||||
/// </summary>
|
||||
[DataField("mapNameTemplate", required: true)]
|
||||
public string StationNameTemplate { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Name generator to use for the station, if any.
|
||||
/// </summary>
|
||||
[DataField("nameGenerator")]
|
||||
public StationNameGenerator? NameGenerator { get; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// System that manages the jobs available on a station, and maybe other things later.
|
||||
/// </summary>
|
||||
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<StationId, StationInfoData> _stationInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of stations currently loaded.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<StationId, StationInfoData> StationInfo => _stationInfo;
|
||||
|
||||
private bool _randomStationOffset = false;
|
||||
private bool _randomStationRotation = false;
|
||||
private float _maxRandomStationOffset = 0.0f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("station");
|
||||
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
|
||||
SubscribeLocalEvent<PreGameMapLoad>(OnPreGameMapLoad);
|
||||
SubscribeLocalEvent<PostGameMapLoad>(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<string, StationId>();
|
||||
|
||||
// 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<BecomesStationComponent>(_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<PartOfStationComponent>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up station info.
|
||||
/// </summary>
|
||||
private void OnRoundEnd(GameRunLevelChangedEvent eventArgs)
|
||||
{
|
||||
if (eventArgs.New == GameRunLevel.PreRoundLobby)
|
||||
_stationInfo = new();
|
||||
}
|
||||
|
||||
public sealed class StationInfoData
|
||||
{
|
||||
public string Name;
|
||||
|
||||
/// <summary>
|
||||
/// Job list associated with the game map.
|
||||
/// </summary>
|
||||
public readonly GameMapPrototype MapPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// The round job list.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, int> _jobList;
|
||||
|
||||
public IReadOnlyDictionary<string, int> JobList => _jobList;
|
||||
|
||||
public StationInfoData(string name, GameMapPrototype mapPrototype, Dictionary<string, int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new station and attaches it to the given grid.
|
||||
/// </summary>
|
||||
/// <param name="mapGrid">grid to attach to</param>
|
||||
/// <param name="mapPrototype">game map prototype of the station</param>
|
||||
/// <param name="stationName">name of the station to assign, if not the default</param>
|
||||
/// <param name="gridComponent">optional grid component of the grid.</param>
|
||||
/// <returns>The ID of the resulting station</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given entity is not a grid.</exception>
|
||||
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<StationComponent>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given grid to the given station.
|
||||
/// </summary>
|
||||
/// <param name="mapGrid">grid to attach</param>
|
||||
/// <param name="station">station to attach the grid to</param>
|
||||
/// <param name="gridComponent">optional grid component of the grid.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the given entity is not a grid.</exception>
|
||||
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<StationComponent>(mapGrid);
|
||||
stationComponent.Station = station;
|
||||
|
||||
_sawmill.Info( $"Adding grid {mapGrid}:{gridComponent.GridIndex} to station {station} named {_stationInfo[station].Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to assign a job on the given station.
|
||||
/// Does NOT inform the gameticker that the job roster has changed.
|
||||
/// </summary>
|
||||
/// <param name="stationId">station to assign to</param>
|
||||
/// <param name="job">name of the job</param>
|
||||
/// <returns>assignment success</returns>
|
||||
public bool TryAssignJobToStation(StationId stationId, JobPrototype job)
|
||||
{
|
||||
if (stationId != StationId.Invalid)
|
||||
return _stationInfo[stationId].TryAssignJob(job.ID);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given job is available.
|
||||
/// </summary>
|
||||
/// <param name="stationId">station to check</param>
|
||||
/// <param name="job">name of the job</param>
|
||||
/// <returns>job availability</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
347
Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs
Normal file
347
Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs
Normal file
@@ -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<int, HashSet<string>> _jobsByWeight = default!;
|
||||
private List<int> _orderedWeights = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up some tables used by AssignJobs, including jobs sorted by their weights, and a list of weights in order from highest to lowest.
|
||||
/// </summary>
|
||||
private void InitializeRoundStart()
|
||||
{
|
||||
_jobsByWeight = new Dictionary<int, HashSet<string>>();
|
||||
foreach (var job in _prototypeManager.EnumeratePrototypes<JobPrototype>())
|
||||
{
|
||||
if (!_jobsByWeight.ContainsKey(job.Weight))
|
||||
_jobsByWeight.Add(job.Weight, new HashSet<string>());
|
||||
|
||||
_jobsByWeight[job.Weight].Add(job.ID);
|
||||
}
|
||||
|
||||
_orderedWeights = _jobsByWeight.Keys.OrderByDescending(i => i).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="profiles">The profiles to use for selection.</param>
|
||||
/// <param name="stations">List of stations to assign for.</param>
|
||||
/// <param name="useRoundStartJobs">Whether or not to use the round-start jobs for the stations instead of their current jobs.</param>
|
||||
/// <returns>List of players and their assigned jobs.</returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public Dictionary<NetUserId, (string, EntityUid)> AssignJobs(Dictionary<NetUserId, HumanoidCharacterProfile> profiles, IReadOnlyList<EntityUid> stations, bool useRoundStartJobs = true)
|
||||
{
|
||||
DebugTools.Assert(stations.Count > 0);
|
||||
|
||||
if (profiles.Count == 0)
|
||||
return new Dictionary<NetUserId, (string, EntityUid)>();
|
||||
|
||||
// We need to modify this collection later, so make a copy of it.
|
||||
profiles = profiles.ShallowClone();
|
||||
|
||||
// Player <-> (job, station)
|
||||
var assigned = new Dictionary<NetUserId, (string, EntityUid)>(profiles.Count);
|
||||
|
||||
// The jobs left on the stations. This collection is modified as jobs are assigned to track what's available.
|
||||
var stationJobs = new Dictionary<EntityUid, Dictionary<string, uint?>>();
|
||||
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<EntityUid, Dictionary<string, uint?>>(stations.Count);
|
||||
foreach (var station in stations)
|
||||
{
|
||||
currentlySelectingJobs.Add(station, new Dictionary<string, uint?>());
|
||||
}
|
||||
|
||||
// And these.
|
||||
// Tracks what players are available for a given job in the current iteration of selection.
|
||||
var jobPlayerOptions = new Dictionary<string, HashSet<NetUserId>>();
|
||||
// Tracks the total number of slots for the given stations in the current iteration of selection.
|
||||
var stationTotalSlots = new Dictionary<EntityUid, int>(stations.Count);
|
||||
// The share of the players each station gets in the current iteration of job selection.
|
||||
var stationShares = new Dictionary<EntityUid, int>(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<NetUserId>());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to assign overflow jobs to any player in allPlayersToAssign that is not in assignedJobs.
|
||||
/// </summary>
|
||||
/// <param name="assignedJobs">All assigned jobs.</param>
|
||||
/// <param name="allPlayersToAssign">All players that might need an overflow assigned.</param>
|
||||
/// <param name="profiles">Player character profiles.</param>
|
||||
/// <param name="stations">The stations to consider for spawn location.</param>
|
||||
public void AssignOverflowJobs(ref Dictionary<NetUserId, (string, EntityUid)> assignedJobs,
|
||||
IEnumerable<NetUserId> allPlayersToAssign, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles, IReadOnlyList<EntityUid> stations)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all jobs that the input players have that match the given weight and priority.
|
||||
/// </summary>
|
||||
/// <param name="weight">Weight to find, if any.</param>
|
||||
/// <param name="selectedPriority">Priority to find, if any.</param>
|
||||
/// <param name="profiles">Profiles to look in.</param>
|
||||
/// <returns>Players and a list of their matching jobs.</returns>
|
||||
private Dictionary<NetUserId, List<string>> GetPlayersJobCandidates(int? weight, JobPriority? selectedPriority, Dictionary<NetUserId, HumanoidCharacterProfile> profiles)
|
||||
{
|
||||
var outputDict = new Dictionary<NetUserId, List<string>>(profiles.Count);
|
||||
|
||||
foreach (var (player, profile) in profiles)
|
||||
{
|
||||
var roleBans = _roleBanManager.GetJobBans(player);
|
||||
|
||||
List<string>? 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<string>(profile.JobPriorities.Count);
|
||||
|
||||
availableJobs.Add(jobId);
|
||||
}
|
||||
|
||||
if (availableJobs is not null)
|
||||
outputDict.Add(player, availableJobs);
|
||||
}
|
||||
|
||||
return outputDict;
|
||||
}
|
||||
}
|
||||
498
Content.Server/Station/Systems/StationJobsSystem.cs
Normal file
498
Content.Server/Station/Systems/StationJobsSystem.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Manages job slots for stations.
|
||||
/// </summary>
|
||||
[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!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialized);
|
||||
SubscribeLocalEvent<StationJobsComponent, StationRenamedEvent>(OnStationRenamed);
|
||||
SubscribeLocalEvent<StationJobsComponent, ComponentShutdown>(OnStationDeletion);
|
||||
SubscribeLocalEvent<PlayerJoinedLobbyEvent>(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<StationJobsComponent>(msg.Station);
|
||||
var stationData = Comp<StationDataComponent>(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
|
||||
|
||||
/// <inheritdoc cref="TryAssignJob(Robust.Shared.GameObjects.EntityUid,string,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to assign a job on.</param>
|
||||
/// <param name="job">Job to assign.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
public bool TryAssignJob(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return TryAssignJob(station, job.ID, stationJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to assign the given job once. (essentially, it decrements the slot if possible).
|
||||
/// </summary>
|
||||
/// <param name="station">Station to assign a job on.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to assign.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Whether or not assignment was a success.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public bool TryAssignJob(EntityUid station, string jobPrototypeId, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryAdjustJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,bool,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to adjust the job slot on.</param>
|
||||
/// <param name="job">Job to adjust.</param>
|
||||
/// <param name="amount">Amount to adjust by.</param>
|
||||
/// <param name="createSlot">Whether or not it should create the slot if it doesn't exist.</param>
|
||||
/// <param name="clamp">Whether or not to clamp to zero if you'd remove more jobs than are available.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to adjust the given job slot by the amount provided.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to adjust the job slot on.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to adjust.</param>
|
||||
/// <param name="amount">Amount to adjust by.</param>
|
||||
/// <param name="createSlot">Whether or not it should create the slot if it doesn't exist.</param>
|
||||
/// <param name="clamp">Whether or not to clamp to zero if you'd remove more jobs than are available.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Whether or not slot adjustment was a success.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public bool TryAdjustJobSlot(EntityUid station, string jobPrototypeId, int amount, bool createSlot = false, bool clamp = false,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TrySetJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to adjust the job slot on.</param>
|
||||
/// <param name="jobPrototype">Job prototype to adjust.</param>
|
||||
/// <param name="amount">Amount to set to.</param>
|
||||
/// <param name="createSlot">Whether or not it should create the slot if it doesn't exist.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns></returns>
|
||||
public bool TrySetJobSlot(EntityUid station, JobPrototype jobPrototype, int amount, bool createSlot = false,
|
||||
StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return TrySetJobSlot(station, jobPrototype.ID, amount, createSlot, stationJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the given job slot to the amount provided.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to adjust the job slot on.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to adjust.</param>
|
||||
/// <param name="amount">Amount to set to.</param>
|
||||
/// <param name="createSlot">Whether or not it should create the slot if it doesn't exist.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Whether or not setting the value succeeded.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public bool TrySetJobSlot(EntityUid station, string jobPrototypeId, int amount, bool createSlot = false,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="MakeJobUnlimited(Robust.Shared.GameObjects.EntityUid,string,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to make a job unlimited on.</param>
|
||||
/// <param name="job">Job to make unlimited.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
public void MakeJobUnlimited(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
MakeJobUnlimited(station, job.ID, stationJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the given job have unlimited slots.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to make a job unlimited on.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to make unlimited.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IsJobUnlimited(Robust.Shared.GameObjects.EntityUid,string,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to check.</param>
|
||||
/// <param name="job">Job to check.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
public bool IsJobUnlimited(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return IsJobUnlimited(station, job.ID, stationJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given job is unlimited.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to check.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to check.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Returns if the given slot is unlimited.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryGetJobSlot(Robust.Shared.GameObjects.EntityUid,string,out System.Nullable{uint},Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to get slot info from.</param>
|
||||
/// <param name="job">Job to get slot info for.</param>
|
||||
/// <param name="slots">The number of slots remaining. Null if infinite.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
public bool TryGetJobSlot(EntityUid station, JobPrototype job, out uint? slots, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return TryGetJobSlot(station, job.ID, out slots, stationJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the given job slot.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get slot info from.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to get slot info for.</param>
|
||||
/// <param name="slots">The number of slots remaining. Null if infinite.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Whether or not the slot exists.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
/// <remarks>slots will be null if the slot doesn't exist, as well, so make sure to check the return value.</remarks>
|
||||
public bool TryGetJobSlot(EntityUid station, string jobPrototypeId, out uint? slots, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all jobs available on the station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get jobs for</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Set containing all jobs available.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public IReadOnlySet<string> GetAvailableJobs(EntityUid station, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all overflow jobs available on the station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get jobs for</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Set containing all overflow jobs available.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public IReadOnlySet<string> GetOverflowJobs(EntityUid station, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
if (!Resolve(station, ref stationJobs))
|
||||
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
|
||||
|
||||
return stationJobs.OverflowJobs.ToHashSet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a readonly dictionary of all jobs and their slot info.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get jobs for</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>List of all jobs on the station.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public IReadOnlyDictionary<string, uint?> GetJobs(EntityUid station, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
if (!Resolve(station, ref stationJobs))
|
||||
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
|
||||
|
||||
return stationJobs.JobList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a readonly dictionary of all round-start jobs and their slot info.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get jobs for</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>List of all round-start jobs.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public IReadOnlyDictionary<string, uint?> GetRoundStartJobs(EntityUid station, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
if (!Resolve(station, ref stationJobs))
|
||||
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
|
||||
|
||||
return stationJobs.RoundStartJobList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks at the given priority list, and picks the best available job (optionally with the given exclusions)
|
||||
/// </summary>
|
||||
/// <param name="station">Station to pick from.</param>
|
||||
/// <param name="jobPriorities">The priority list to use for selecting a job.</param>
|
||||
/// <param name="pickOverflows">Whether or not to pick from the overflow list.</param>
|
||||
/// <param name="disallowedJobs">A set of disallowed jobs, if any.</param>
|
||||
/// <returns>The selected job, if any.</returns>
|
||||
public string? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary<string, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet<string>? 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<EntityUid, string>(), new Dictionary<EntityUid, Dictionary<string, uint?>>());
|
||||
|
||||
/// <summary>
|
||||
/// Assembles an event from the current available-to-play jobs.
|
||||
/// This is moderately expensive to construct.
|
||||
/// </summary>
|
||||
/// <returns>The event.</returns>
|
||||
private TickerJobsAvailableEvent GenerateJobsAvailableEvent()
|
||||
{
|
||||
// If late join is disallowed, return no available jobs.
|
||||
if (_gameTicker.DisallowLateJoin)
|
||||
return new TickerJobsAvailableEvent(new Dictionary<EntityUid, string>(), new Dictionary<EntityUid, Dictionary<string, uint?>>());
|
||||
|
||||
var jobs = new Dictionary<EntityUid, Dictionary<string, uint?>>();
|
||||
var stationNames = new Dictionary<EntityUid, string>();
|
||||
|
||||
foreach (var station in _stationSystem.Stations)
|
||||
{
|
||||
var list = Comp<StationJobsComponent>(station).JobList.ToDictionary(x => x.Key, x => x.Value);
|
||||
jobs.Add(station, list);
|
||||
stationNames.Add(station, Name(station));
|
||||
}
|
||||
return new TickerJobsAvailableEvent(stationNames, jobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the cached available jobs. Moderately expensive.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
206
Content.Server/Station/Systems/StationSpawningSystem.cs
Normal file
206
Content.Server/Station/Systems/StationSpawningSystem.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Manages spawning into the game, tracking available spawn points.
|
||||
/// Also provides helpers for spawning in the player's mob.
|
||||
/// </summary>
|
||||
[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!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialized);
|
||||
}
|
||||
|
||||
private void OnStationInitialized(StationInitializedEvent ev)
|
||||
{
|
||||
AddComp<StationSpawningComponent>(ev.Station);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to spawn a player character onto the given station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to spawn onto.</param>
|
||||
/// <param name="job">The job to assign, if any.</param>
|
||||
/// <param name="profile">The character profile to use, if any.</param>
|
||||
/// <param name="stationSpawning">Resolve pattern, the station spawning component for the station.</param>
|
||||
/// <returns>The resulting player character, if any.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
/// <remarks>
|
||||
/// This only spawns the character, and does none of the mind-related setup you'd need for it to be playable.
|
||||
/// </remarks>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="coordinates">Coordinates to spawn the character at.</param>
|
||||
/// <param name="job">Job to assign to the character, if any.</param>
|
||||
/// <param name="profile">Appearance profile to use for the character.</param>
|
||||
/// <returns>The spawned entity</returns>
|
||||
public EntityUid SpawnPlayerMob(EntityCoordinates coordinates, Job? job, HumanoidCharacterProfile? profile)
|
||||
{
|
||||
var entity = EntityManager.SpawnEntity(
|
||||
_prototypeManager.Index<SpeciesPrototype>(profile?.Species ?? SpeciesManager.DefaultSpecies).Prototype,
|
||||
coordinates);
|
||||
|
||||
if (job?.StartingGear != null)
|
||||
{
|
||||
var startingGear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
|
||||
EquipStartingGear(entity, startingGear, profile);
|
||||
if (profile != null)
|
||||
EquipIdCard(entity, profile.Name, job.Prototype);
|
||||
}
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_humanoidAppearanceSystem.UpdateFromProfile(entity, profile);
|
||||
EntityManager.GetComponent<MetaDataComponent>(entity).EntityName = profile.Name;
|
||||
}
|
||||
|
||||
foreach (var jobSpecial in job?.Prototype.Special ?? Array.Empty<JobSpecial>())
|
||||
{
|
||||
jobSpecial.AfterEquip(entity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips starting gear onto the given entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity to load out.</param>
|
||||
/// <param name="startingGear">Starting gear to use.</param>
|
||||
/// <param name="profile">Character profile to use, if any.</param>
|
||||
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<TransformComponent>(entity).Coordinates);
|
||||
_inventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryComp(entity, out HandsComponent? handsComponent))
|
||||
return;
|
||||
|
||||
var inhand = startingGear.Inhand;
|
||||
var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
|
||||
foreach (var (hand, prototype) in inhand)
|
||||
{
|
||||
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
|
||||
_handsSystem.TryPickup(entity, inhandEntity, hand, checkActionBlocker: false, handsComp: handsComponent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips an ID card and PDA onto the given entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity to load out.</param>
|
||||
/// <param name="characterName">Character name to use for the ID.</param>
|
||||
/// <param name="jobPrototype">Job prototype to use for the PDA and ID.</param>
|
||||
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<AccessComponent>(card.Owner);
|
||||
var accessTags = access.Tags;
|
||||
accessTags.UnionWith(jobPrototype.Access);
|
||||
_pdaSystem.SetOwner(pdaComponent, characterName);
|
||||
}
|
||||
|
||||
|
||||
#endregion Player spawning helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class PlayerSpawningEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public EntityUid? SpawnResult;
|
||||
/// <summary>
|
||||
/// The job to use, if any.
|
||||
/// </summary>
|
||||
public readonly Job? Job;
|
||||
/// <summary>
|
||||
/// The profile to use, if any.
|
||||
/// </summary>
|
||||
public readonly HumanoidCharacterProfile? HumanoidCharacterProfile;
|
||||
/// <summary>
|
||||
/// The target station, if any.
|
||||
/// </summary>
|
||||
public readonly EntityUid? Station;
|
||||
|
||||
public PlayerSpawningEvent(Job? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
|
||||
{
|
||||
Job = job;
|
||||
HumanoidCharacterProfile = humanoidCharacterProfile;
|
||||
Station = station;
|
||||
}
|
||||
}
|
||||
407
Content.Server/Station/Systems/StationSystem.cs
Normal file
407
Content.Server/Station/Systems/StationSystem.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<EntityUid> _stations = new();
|
||||
|
||||
/// <summary>
|
||||
/// All stations that currently exist.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public IReadOnlySet<EntityUid> Stations => _stations;
|
||||
|
||||
private bool _randomStationOffset;
|
||||
private bool _randomStationRotation;
|
||||
private float _maxRandomStationOffset;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("station");
|
||||
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
|
||||
SubscribeLocalEvent<PreGameMapLoad>(OnPreGameMapLoad);
|
||||
SubscribeLocalEvent<PostGameMapLoad>(OnPostGameMapLoad);
|
||||
SubscribeLocalEvent<StationDataComponent, ComponentAdd>(OnStationStartup);
|
||||
SubscribeLocalEvent<StationDataComponent, ComponentShutdown>(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<string, List<GridId>>();
|
||||
|
||||
void AddGrid(string station, GridId grid)
|
||||
{
|
||||
if (dict.ContainsKey(station))
|
||||
{
|
||||
dict[station].Add(grid);
|
||||
}
|
||||
else
|
||||
{
|
||||
dict[station] = new List<GridId> {grid};
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over all BecomesStation
|
||||
foreach (var grid in ev.Grids)
|
||||
{
|
||||
// We still setup the grid
|
||||
if (!TryComp<BecomesStationComponent>(_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<PartOfStationComponent>(_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
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generates a station name from the given config.
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
/// <returns></returns>
|
||||
public static string GenerateStationName(StationConfig config)
|
||||
{
|
||||
return config.NameGenerator is not null
|
||||
? config.NameGenerator.FormatName(config.StationNameTemplate)
|
||||
: config.StationNameTemplate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new station with the given information.
|
||||
/// </summary>
|
||||
/// <param name="stationConfig">The game map prototype used, if any.</param>
|
||||
/// <param name="gridIds">All grids that should be added to the station.</param>
|
||||
/// <param name="name">Optional override for the station name.</param>
|
||||
/// <returns>The initialized station.</returns>
|
||||
public EntityUid InitializeNewStation(StationConfig? stationConfig, IEnumerable<EntityUid>? 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<StationDataComponent>(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<EntityUid>())
|
||||
{
|
||||
AddGridToStation(station, grid, null, data);
|
||||
}
|
||||
|
||||
return station;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given grid to a station.
|
||||
/// </summary>
|
||||
/// <param name="mapGrid">Grid to attach.</param>
|
||||
/// <param name="station">Station to attach the grid to.</param>
|
||||
/// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
|
||||
/// <param name="stationData">Resolve pattern, station data component of station.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
|
||||
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<StationMemberComponent>(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})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given grid from a station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to remove the grid from.</param>
|
||||
/// <param name="mapGrid">Grid to remove</param>
|
||||
/// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
|
||||
/// <param name="stationData">Resolve pattern, station data component of station.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
|
||||
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<StationMemberComponent>(mapGrid);
|
||||
stationData.Grids.Remove(gridComponent.GridIndex);
|
||||
|
||||
RaiseLocalEvent(station, new StationGridRemovedEvent(gridComponent.GridIndex));
|
||||
_sawmill.Info($"Removing grid {mapGrid}:{gridComponent.GridIndex} from station {Name(station)} ({station})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames the given station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to rename.</param>
|
||||
/// <param name="name">The new name to apply.</param>
|
||||
/// <param name="loud">Whether or not to announce the rename.</param>
|
||||
/// <param name="stationData">Resolve pattern, station data component of station.</param>
|
||||
/// <param name="metaData">Resolve pattern, metadata component of station.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the given station.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to delete.</param>
|
||||
/// <param name="stationData">Resolve pattern, station data component of station.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to)
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity to find the owner of.</param>
|
||||
/// <param name="xform">Resolve pattern, transform of the entity.</param>
|
||||
/// <returns>The owning station, if any.</returns>
|
||||
/// <remarks>
|
||||
/// This does not remember what station an entity started on, it simply checks where it is currently located.
|
||||
/// </remarks>
|
||||
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<IMapGridComponent>(entity, out _))
|
||||
{
|
||||
// We are the station, just check ourselves.
|
||||
return CompOrNull<StationMemberComponent>(entity)?.Station;
|
||||
}
|
||||
|
||||
if (xform.GridID == GridId.Invalid)
|
||||
{
|
||||
Logger.Debug("A");
|
||||
return null;
|
||||
}
|
||||
|
||||
var grid = _mapManager.GetGridEuid(xform.GridID);
|
||||
|
||||
return CompOrNull<StationMemberComponent>(grid)?.Station;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast event fired when a station is first set up.
|
||||
/// This is the ideal point to add components to it.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class StationInitializedEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Station this event is for.
|
||||
/// </summary>
|
||||
public EntityUid Station;
|
||||
|
||||
public StationInitializedEvent(EntityUid station)
|
||||
{
|
||||
Station = station;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directed event fired on a station when a grid becomes a member of the station.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class StationGridAddedEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the grid added to the station.
|
||||
/// </summary>
|
||||
public GridId GridId;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the event was fired during station setup,
|
||||
/// so that it can be ignored if StationInitializedEvent was already handled.
|
||||
/// </summary>
|
||||
public bool IsSetup;
|
||||
|
||||
public StationGridAddedEvent(GridId gridId, bool isSetup)
|
||||
{
|
||||
GridId = gridId;
|
||||
IsSetup = isSetup;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directed event fired on a station when a grid is no longer a member of the station.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class StationGridRemovedEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of the grid removed from the station.
|
||||
/// </summary>
|
||||
public GridId GridId;
|
||||
|
||||
public StationGridRemovedEvent(GridId gridId)
|
||||
{
|
||||
GridId = gridId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directed event fired on a station when it is renamed.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class StationRenamedEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Prior name of the station.
|
||||
/// </summary>
|
||||
public string OldName;
|
||||
|
||||
/// <summary>
|
||||
/// New name of the station.
|
||||
/// </summary>
|
||||
public string NewName;
|
||||
|
||||
public StationRenamedEvent(string oldName, string newName)
|
||||
{
|
||||
OldName = oldName;
|
||||
NewName = newName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StationSystem>().StationInfo.Values.ToList());
|
||||
var jobList = chosenStation.JobList.Keys.Where(x => !_prototypeManager.Index<JobPrototype>(x).IsHead).ToList();
|
||||
var stationSystem = EntitySystem.Get<StationSystem>();
|
||||
var stationJobsSystem = EntitySystem.Get<StationJobsSystem>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<StationComponent>().ToArray()).Station;
|
||||
base.Startup();
|
||||
|
||||
if (_stationSystem.Stations.Count == 0)
|
||||
{
|
||||
Running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_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<IMapManager>();
|
||||
// Account for split stations by just randomly picking a piece of it.
|
||||
var possibleTargets = _entityManager.EntityQuery<StationComponent>()
|
||||
.Where(x => x.Station == _target).ToArray();
|
||||
StationComponent tempQualifier = _robustRandom.Pick(possibleTargets);
|
||||
var stationEnt = (tempQualifier).Owner;
|
||||
var possibleTargets = _entityManager.GetComponent<StationDataComponent>(_target).Grids;
|
||||
if (possibleTargets.Count == 0)
|
||||
{
|
||||
Running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var stationEnt = _robustRandom.Pick(possibleTargets);
|
||||
|
||||
if (!_entityManager.TryGetComponent<IMapGridComponent>(stationEnt, out var grid))
|
||||
return;
|
||||
|
||||
@@ -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<IRobustRandom>();
|
||||
entityManager ??= IoCManager.Resolve<IEntityManager>();
|
||||
IoCManager.Resolve(ref robustRandom, ref entityManager, ref mapManager);
|
||||
entityManager.EntitySysManager.Resolve(ref stationSystem);
|
||||
|
||||
targetCoords = EntityCoordinates.Invalid;
|
||||
targetStation = robustRandom.Pick(entityManager.EntityQuery<StationComponent>().ToArray()).Station;
|
||||
var t = targetStation; // thanks C#
|
||||
var possibleTargets = entityManager.EntityQuery<StationComponent>()
|
||||
.Where(x => x.Station == t).ToArray();
|
||||
targetGrid = robustRandom.Pick(possibleTargets).Owner;
|
||||
targetStation = robustRandom.Pick(stationSystem.Stations);
|
||||
var possibleTargets = entityManager.GetComponent<StationDataComponent>(targetStation).Grids;
|
||||
if (possibleTargets.Count == 0)
|
||||
{
|
||||
targetGrid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!entityManager.TryGetComponent<IMapGridComponent>(targetGrid!, out var gridComp))
|
||||
targetGrid = robustRandom.Pick(possibleTargets);
|
||||
|
||||
if (!entityManager.TryGetComponent<IMapGridComponent>(targetGrid, out var gridComp))
|
||||
return false;
|
||||
var grid = gridComp.Grid;
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// The Status of the Player in the lobby (ready, observer, ...)
|
||||
/// </summary>
|
||||
public Dictionary<StationId, Dictionary<string, int>> JobsAvailableByStation { get; }
|
||||
public Dictionary<StationId, string> StationNames { get; }
|
||||
public Dictionary<EntityUid, Dictionary<string, uint?>> JobsAvailableByStation { get; }
|
||||
public Dictionary<EntityUid, string> StationNames { get; }
|
||||
|
||||
public TickerJobsAvailableEvent(Dictionary<StationId, string> stationNames, Dictionary<StationId, Dictionary<string, int>> jobsAvailableByStation)
|
||||
public TickerJobsAvailableEvent(Dictionary<EntityUid, string> stationNames, Dictionary<EntityUid, Dictionary<string, uint?>> jobsAvailableByStation)
|
||||
{
|
||||
StationNames = stationNames;
|
||||
JobsAvailableByStation = jobsAvailableByStation;
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[DataField("head")]
|
||||
public bool IsHead { get; private set; }
|
||||
[DataField("weight")]
|
||||
public int Weight { get; private set; }
|
||||
|
||||
[DataField("startingGear", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
public string? StartingGear { get; private set; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -56,4 +56,10 @@ entities:
|
||||
fixtures: []
|
||||
bodyType: Dynamic
|
||||
type: Physics
|
||||
- uid: 1
|
||||
type: SpawnPointLatejoin
|
||||
components:
|
||||
- parent: 0
|
||||
pos: 0,0
|
||||
type: Transform
|
||||
...
|
||||
|
||||
@@ -125,4 +125,10 @@ entities:
|
||||
type: Gravity
|
||||
- chunkCollection: {}
|
||||
type: DecalGrid
|
||||
- uid: 1
|
||||
type: SpawnPointLatejoin
|
||||
components:
|
||||
- parent: 0
|
||||
pos: 0,0
|
||||
type: Transform
|
||||
...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
- type: gameMap
|
||||
id: bagelstation
|
||||
mapName: 'Bagel Station'
|
||||
mapPath: /Maps/bagel.yml
|
||||
minPlayers: 35
|
||||
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'
|
||||
mapPath: /Maps/bagel.yml
|
||||
minPlayers: 35
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
- type: gameMap
|
||||
id: delta
|
||||
mapName: 'Delta Station'
|
||||
mapPath: /Maps/delta.yml
|
||||
minPlayers: 60
|
||||
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'
|
||||
mapPath: /Maps/delta.yml
|
||||
minPlayers: 60
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
|
||||
@@ -1,97 +1,14 @@
|
||||
- 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'
|
||||
mapPath: /Maps/nss_pillar.yml
|
||||
minPlayers: 40
|
||||
stations:
|
||||
Pillar:
|
||||
mapNameTemplate: '{0} NSSPillar {1}'
|
||||
nameGenerator:
|
||||
!type:NanotrasenNameGenerator
|
||||
prefixCreator: '14'
|
||||
mapPath: /Maps/nss_pillar.yml
|
||||
minPlayers: 40
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
@@ -126,14 +43,16 @@
|
||||
- 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
|
||||
stations:
|
||||
Reach:
|
||||
mapNameTemplate: '{0} Reach {1}'
|
||||
nameGenerator:
|
||||
!type:NanotrasenNameGenerator
|
||||
prefixCreator: '14'
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
@@ -156,13 +75,15 @@
|
||||
- type: gameMap
|
||||
id: dart
|
||||
mapName: 'Dart'
|
||||
mapPath: /Maps/dart.yml
|
||||
minPlayers: 0
|
||||
votable: false
|
||||
stations:
|
||||
Station: #TODO: Mapper, add a BecomesStation component to the primary grid of the map.
|
||||
mapNameTemplate: '{0} Dart {1}'
|
||||
nameGenerator:
|
||||
!type:NanotrasenNameGenerator
|
||||
prefixCreator: '14'
|
||||
mapPath: /Maps/dart.yml
|
||||
minPlayers: 0
|
||||
votable: false
|
||||
overflowJobs: []
|
||||
availableJobs:
|
||||
Captain: [ 1, 1 ]
|
||||
@@ -170,13 +91,15 @@
|
||||
- type: gameMap
|
||||
id: moonrise
|
||||
mapName: 'Moonrise ERC'
|
||||
mapPath: /Maps/moonrise.yml
|
||||
minPlayers: 0
|
||||
votable: false
|
||||
stations:
|
||||
Station:
|
||||
mapNameTemplate: '{0} Moonrise {1}'
|
||||
nameGenerator:
|
||||
!type:NanotrasenNameGenerator
|
||||
prefixCreator: 'VG'
|
||||
mapPath: /Maps/moonrise.yml
|
||||
minPlayers: 0
|
||||
votable: false
|
||||
overflowJobs: []
|
||||
availableJobs:
|
||||
Captain: [ 1, 1 ]
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
- type: gameMap
|
||||
id: marathonstation
|
||||
mapName: 'Marathon Station'
|
||||
mapPath: /Maps/marathon.yml
|
||||
minPlayers: 35
|
||||
maxPlayers: 70
|
||||
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'
|
||||
mapPath: /Maps/marathon.yml
|
||||
minPlayers: 35
|
||||
maxPlayers: 70
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
@@ -22,7 +24,7 @@
|
||||
Captain: [ 1, 1 ]
|
||||
HeadOfPersonnel: [ 1, 1 ]
|
||||
ChiefEngineer: [ 1, 1 ]
|
||||
StationEngineer: [ 4, 4 ]
|
||||
StationEngineer: [ 4, 6 ]
|
||||
ChiefMedicalOfficer: [ 1, 1 ]
|
||||
MedicalDoctor: [ 3, 4 ]
|
||||
Chemist: [ 2, 3 ]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
- type: gameMap
|
||||
id: moosestation
|
||||
mapName: 'Moose Station'
|
||||
mapPath: /Maps/moose.yml
|
||||
minPlayers: 0
|
||||
maxPlayers: 35
|
||||
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'
|
||||
mapPath: /Maps/moose.yml
|
||||
minPlayers: 0
|
||||
maxPlayers: 35
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
|
||||
40
Resources/Prototypes/Maps/packedstation.yml
Normal file
40
Resources/Prototypes/Maps/packedstation.yml
Normal file
@@ -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]
|
||||
@@ -1,18 +1,20 @@
|
||||
- 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
|
||||
stations:
|
||||
Saltern:
|
||||
mapNameTemplate: '{0} Saltern {1}'
|
||||
nameGenerator:
|
||||
!type:NanotrasenNameGenerator
|
||||
prefixCreator: '14'
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
CargoTechnician: [ 2, 2 ]
|
||||
CargoTechnician: [ 1, 2 ]
|
||||
Passenger: [ -1, -1 ]
|
||||
Bartender: [ 1, 1 ]
|
||||
Botanist: [ 2, 2 ]
|
||||
@@ -23,21 +25,20 @@
|
||||
Captain: [ 1, 1 ]
|
||||
HeadOfPersonnel: [ 1, 1 ]
|
||||
ChiefEngineer: [ 1, 1 ]
|
||||
StationEngineer: [ 3, 3 ]
|
||||
StationEngineer: [ 2, 3 ]
|
||||
ChiefMedicalOfficer: [ 1, 1 ]
|
||||
MedicalDoctor: [ 2, 3 ]
|
||||
Chemist: [ 1, 1 ]
|
||||
ResearchDirector: [ 1, 1 ]
|
||||
Scientist: [ 2, 3 ]
|
||||
HeadOfSecurity: [ 1, 1 ]
|
||||
SecurityOfficer: [ 3, 3 ]
|
||||
SecurityOfficer: [ 2, 3 ]
|
||||
Chaplain: [ 1, 1 ]
|
||||
Librarian: [ 1, 1 ]
|
||||
Musician: [ 1, 1 ]
|
||||
Lawyer: [ 1, 1 ]
|
||||
SalvageSpecialist: [ 2, 2 ]
|
||||
SalvageSpecialist: [ 1, 2 ]
|
||||
Quartermaster: [ 1, 1 ]
|
||||
AtmosphericTechnician: [ 2, 2 ]
|
||||
TechnicalAssistant: [ 2, 2 ]
|
||||
MedicalIntern: [ 2, 2 ]
|
||||
ServiceWorker: [ 2, 2 ]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
- type: gameMap
|
||||
id: splitstation
|
||||
mapName: 'Split Station'
|
||||
mapPath: /Maps/splitstation.yml
|
||||
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'
|
||||
mapPath: /Maps/splitstation.yml
|
||||
minPlayers: 60
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
mapPath: /Maps/Test/empty.yml
|
||||
minPlayers: 0
|
||||
votable: false
|
||||
stations:
|
||||
Station: #TODO: Add a BecomesStation to empty.yml
|
||||
mapNameTemplate: "Empty"
|
||||
overflowJobs:
|
||||
- Passenger
|
||||
availableJobs:
|
||||
Passenger: [ -1, -1 ]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: job
|
||||
id: Quartermaster
|
||||
name: "quartermaster"
|
||||
head: true
|
||||
weight: 10
|
||||
startingGear: QuartermasterGear
|
||||
departments:
|
||||
- Cargo
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: job
|
||||
id: Captain
|
||||
name: "captain"
|
||||
head: true
|
||||
weight: 20
|
||||
startingGear: CaptainGear
|
||||
departments:
|
||||
- Command
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: job
|
||||
id: HeadOfPersonnel
|
||||
name: "head of personnel"
|
||||
head: true
|
||||
weight: 20
|
||||
startingGear: HoPGear
|
||||
departments:
|
||||
- Command
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: job
|
||||
id: ChiefEngineer
|
||||
name: "chief engineer"
|
||||
head: true
|
||||
weight: 10
|
||||
startingGear: ChiefEngineerGear
|
||||
departments:
|
||||
- Command
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- type: job
|
||||
id: ChiefMedicalOfficer
|
||||
name: "chief medical officer"
|
||||
head: true
|
||||
weight: 10
|
||||
startingGear: CMOGear
|
||||
departments:
|
||||
- Command
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: job
|
||||
id: ResearchDirector
|
||||
name: "research director"
|
||||
head: true
|
||||
weight: 10
|
||||
startingGear: ResearchDirectorGear
|
||||
departments:
|
||||
- Command
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- type: job
|
||||
id: HeadOfSecurity
|
||||
name: "head of security"
|
||||
head: true
|
||||
weight: 10
|
||||
startingGear: HoSGear
|
||||
departments:
|
||||
- Command
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Computus/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Constructible/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dentification/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Diethylamine/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Drainable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=euid/@EntryIndexedValue">True</s:Boolean>
|
||||
@@ -305,5 +306,6 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Wirecutter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xeno/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xenoarchaeology/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=xform/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=yamls/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Zumos/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user