| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using Content.Server.GameTicking;
- using Content.Server.Station.Components;
- using Content.Shared.CCVar;
- using Content.Shared.FixedPoint;
- using Content.Shared.GameTicking;
- using Content.Shared.Preferences;
- using Content.Shared.Roles;
- using JetBrains.Annotations;
- using Robust.Server.Player;
- using Robust.Shared.Configuration;
- using Robust.Shared.Network;
- using Robust.Shared.Player;
- using Robust.Shared.Prototypes;
- 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 IPlayerManager _player = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly GameTicker _gameTicker = default!;
- /// <inheritdoc/>
- public override void Initialize()
- {
- SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialized);
- SubscribeLocalEvent<StationJobsComponent, ComponentInit>(OnInit);
- SubscribeLocalEvent<StationJobsComponent, StationRenamedEvent>(OnStationRenamed);
- SubscribeLocalEvent<StationJobsComponent, ComponentShutdown>(OnStationDeletion);
- SubscribeLocalEvent<PlayerJoinedLobbyEvent>(OnPlayerJoinedLobby);
- Subs.CVar(_configurationManager, CCVars.GameDisallowLateJoins, _ => UpdateJobsAvailable(), true);
- }
- private void OnInit(Entity<StationJobsComponent> ent, ref ComponentInit args)
- {
- ent.Comp.MidRoundTotalJobs = ent.Comp.SetupAvailableJobs.Values
- .Select(x => Math.Max(x[1], 0))
- .Sum();
- ent.Comp.OverflowJobs = ent.Comp.SetupAvailableJobs
- .Where(x => x.Value[0] < 0)
- .Select(x => x.Key)
- .ToHashSet();
- }
- public override void Update(float _)
- {
- if (_availableJobsDirty)
- {
- _cachedAvailableJobs = GenerateJobsAvailableEvent();
- RaiseNetworkEvent(_cachedAvailableJobs, Filter.Empty().AddPlayers(_player.Sessions));
- _availableJobsDirty = false;
- }
- }
- 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)
- {
- if (!TryComp<StationJobsComponent>(msg.Station, out var stationJobs))
- return;
- stationJobs.JobList = stationJobs.SetupAvailableJobs.ToDictionary(
- x => x.Key,
- x=> (int?)(x.Value[1] < 0 ? null : x.Value[1]));
- stationJobs.TotalJobs = stationJobs.JobList.Values.Select(x => x ?? 0).Sum();
- UpdateJobsAvailable();
- }
- #region Public API
- /// <inheritdoc cref="TryAssignJob(Robust.Shared.GameObjects.EntityUid,string,NetUserId,Content.Server.Station.Components.StationJobsComponent?)"/>
- /// <param name="station">Station to assign a job on.</param>
- /// <param name="job">Job to assign.</param>
- /// <param name="netUserId">The net user ID of the player we're assigning this job to.</param>
- /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
- public bool TryAssignJob(EntityUid station, JobPrototype job, NetUserId netUserId, StationJobsComponent? stationJobs = null)
- {
- return TryAssignJob(station, job.ID, netUserId, 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="netUserId">The net user ID of the player we're assigning this job to.</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, NetUserId netUserId, StationJobsComponent? stationJobs = null)
- {
- if (!Resolve(station, ref stationJobs, false))
- return false;
- if (!TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs))
- return false;
- stationJobs.PlayerJobs.TryAdd(netUserId, new());
- stationJobs.PlayerJobs[netUserId].Add(jobPrototypeId);
- return true;
- }
- /// <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.
- if (amount == 0)
- return true;
- switch (jobList.TryGetValue(jobPrototypeId, out var available))
- {
- case false when amount < 0:
- return false;
- case false:
- if (!createSlot)
- return false;
- stationJobs.TotalJobs += amount;
- jobList[jobPrototypeId] = amount;
- UpdateJobsAvailable();
- return true;
- case true:
- // Job is unlimited so just say we adjusted it and do nothing.
- if (available is not {} avail)
- return true;
- // Would remove more jobs than we have available.
- if (available + amount < 0 && !clamp)
- return false;
- jobList[jobPrototypeId] = Math.Max(avail + amount, 0);
- stationJobs.TotalJobs = jobList.Values.Select(x => x ?? 0).Sum();
- UpdateJobsAvailable();
- return true;
- }
- }
- public bool TryGetPlayerJobs(EntityUid station,
- NetUserId userId,
- [NotNullWhen(true)] out List<ProtoId<JobPrototype>>? jobs,
- StationJobsComponent? jobsComponent = null)
- {
- jobs = null;
- if (!Resolve(station, ref jobsComponent, false))
- return false;
- return jobsComponent.PlayerJobs.TryGetValue(userId, out jobs);
- }
- public bool TryRemovePlayerJobs(EntityUid station,
- NetUserId userId,
- StationJobsComponent? jobsComponent = null)
- {
- if (!Resolve(station, ref jobsComponent, false))
- return false;
- return jobsComponent.PlayerJobs.Remove(userId);
- }
- /// <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] = amount;
- UpdateJobsAvailable();
- return true;
- case true:
- stationJobs.TotalJobs += amount - (jobList[jobPrototypeId] ?? 0);
- jobList[jobPrototypeId] = 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.TryGetValue(jobPrototypeId, out var existing))
- stationJobs.TotalJobs -= existing ?? 0;
- 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));
- return stationJobs.JobList.TryGetValue(jobPrototypeId, out var job) && job == null;
- }
- /// <inheritdoc cref="TryGetJobSlot(Robust.Shared.GameObjects.EntityUid,string,out System.Nullable{uint},Content.Server.Station.Components.StationJobsComponent?)"/>
- /// <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 int? 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 int? slots, StationJobsComponent? stationJobs = null)
- {
- if (!Resolve(station, ref stationJobs))
- throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
- return stationJobs.JobList.TryGetValue(jobPrototypeId, out slots);
- }
- /// <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 IEnumerable<ProtoId<JobPrototype>> GetAvailableJobs(EntityUid station, StationJobsComponent? stationJobs = null)
- {
- if (!Resolve(station, ref stationJobs))
- throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
- return stationJobs.JobList
- .Where(x => x.Value != 0)
- .Select(x => x.Key);
- }
- /// <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<ProtoId<JobPrototype>> GetOverflowJobs(EntityUid station, StationJobsComponent? stationJobs = null)
- {
- if (!Resolve(station, ref stationJobs))
- throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
- return stationJobs.OverflowJobs;
- }
- /// <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<ProtoId<JobPrototype>, int?> GetJobs(EntityUid station, StationJobsComponent? stationJobs = null)
- {
- if (!Resolve(station, ref stationJobs))
- throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
- 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 Dictionary<ProtoId<JobPrototype>, int?> GetRoundStartJobs(EntityUid station, StationJobsComponent? stationJobs = null)
- {
- if (!Resolve(station, ref stationJobs))
- throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
- return stationJobs.SetupAvailableJobs.ToDictionary(
- x => x.Key,
- x=> (int?)(x.Value[0] < 0 ? null : x.Value[0]));
- }
- /// <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 ProtoId<JobPrototype>? PickBestAvailableJobWithPriority(EntityUid station, IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities, bool pickOverflows, IReadOnlySet<ProtoId<JobPrototype>>? disallowedJobs = null)
- {
- if (station == EntityUid.Invalid)
- return null;
- var available = GetAvailableJobs(station);
- bool TryPick(JobPriority priority, [NotNullWhen(true)] out ProtoId<JobPrototype>? 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);
- if (overflows.Count == 0)
- return null;
- return _random.Pick(overflows);
- }
- #endregion Public API
- #region Latejoin job management
- private bool _availableJobsDirty;
- private TickerJobsAvailableEvent _cachedAvailableJobs = new(new(), new());
- /// <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(), new());
- var jobs = new Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>>();
- var stationNames = new Dictionary<NetEntity, string>();
- var query = EntityQueryEnumerator<StationJobsComponent>();
- while (query.MoveNext(out var station, out var comp))
- {
- var netStation = GetNetEntity(station);
- var list = comp.JobList.ToDictionary(x => x.Key, x => x.Value);
- jobs.Add(netStation, list);
- stationNames.Add(netStation, 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.Channel);
- }
- private void OnStationRenamed(EntityUid uid, StationJobsComponent component, StationRenamedEvent args)
- {
- UpdateJobsAvailable();
- }
- #endregion
- }
|