diff --git a/Content.Server/Access/Systems/PresetIdCardSystem.cs b/Content.Server/Access/Systems/PresetIdCardSystem.cs index ad706614da..c3d699fc08 100644 --- a/Content.Server/Access/Systems/PresetIdCardSystem.cs +++ b/Content.Server/Access/Systems/PresetIdCardSystem.cs @@ -1,4 +1,7 @@ using Content.Server.Access.Components; +using Content.Server.GameTicking; +using Content.Server.Station.Components; +using Content.Server.Station.Systems; using Content.Shared.Access.Systems; using Content.Shared.Roles; using Robust.Shared.Prototypes; @@ -10,16 +13,50 @@ namespace Content.Server.Access.Systems [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IdCardSystem _cardSystem = default!; [Dependency] private readonly AccessSystem _accessSystem = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; public override void Initialize() { - base.Initialize(); SubscribeLocalEvent(OnMapInit); + + SubscribeLocalEvent(PlayerJobsAssigned); + } + + private void PlayerJobsAssigned(RulePlayerJobsAssignedEvent ev) + { + // Go over all ID cards and make sure they're correctly configured for extended access. + + foreach (var card in EntityQuery()) + { + var station = _stationSystem.GetOwningStation(card.Owner); + + // If we're not on an extended access station, the ID is already configured correctly from MapInit. + if (station == null || !Comp(station.Value).ExtendedAccess) + return; + + SetupIdAccess(card.Owner, card, true); + } } private void OnMapInit(EntityUid uid, PresetIdCardComponent id, MapInitEvent args) { - if (id.JobName == null) return; + // If a preset ID card is spawned on a station at setup time, + // the station may not exist, + // or may not yet know whether it is on extended access (players not spawned yet). + // PlayerJobsAssigned makes sure extended access is configured correctly in that case. + + var station = _stationSystem.GetOwningStation(id.Owner); + var extended = false; + if (station != null) + extended = Comp(station.Value).ExtendedAccess; + + SetupIdAccess(uid, id, extended); + } + + private void SetupIdAccess(EntityUid uid, PresetIdCardComponent id, bool extended) + { + if (id.JobName == null) + return; if (!_prototypeManager.TryIndex(id.JobName, out JobPrototype? job)) { @@ -27,9 +64,7 @@ namespace Content.Server.Access.Systems return; } - // set access for access component - _accessSystem.TrySetTags(uid, job.Access); - _accessSystem.TryAddGroups(uid, job.AccessGroups); + _accessSystem.SetAccessToJob(uid, job, extended); // and also change job title on a card id _cardSystem.TryChangeJobTitle(uid, job.Name); diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index dc91f66e92..d5f5f06daa 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -6,6 +6,7 @@ using Content.Server.Players; using Content.Server.Roles; using Content.Server.Spawners.Components; using Content.Server.Speech.Components; +using Content.Server.Station.Components; using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Ghost; @@ -59,6 +60,15 @@ namespace Content.Server.GameTicking _stationJobs.AssignOverflowJobs(ref assignedJobs, playerNetIds, profiles, _stationSystem.Stations.ToList()); + // Calculate extended access for stations. + var stationJobCounts = _stationSystem.Stations.ToDictionary(e => e, _ => 0); + foreach (var (_, (_, station)) in assignedJobs) + { + stationJobCounts[station] += 1; + } + + _stationJobs.CalcExtendedAccess(stationJobCounts); + // Spawn everybody in! foreach (var (player, (job, station)) in assignedJobs) { @@ -173,6 +183,14 @@ namespace Content.Server.GameTicking else _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}."); + // Make sure they're aware of extended access. + if (Comp(station).ExtendedAccess + && (jobPrototype.ExtendedAccess.Count > 0 + || jobPrototype.ExtendedAccessGroups.Count > 0)) + { + _chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages")); + } + // 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); RaiseLocalEvent(mob, aev); diff --git a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs b/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs index 4c177b0d4d..f0e936859d 100644 --- a/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs +++ b/Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs @@ -31,14 +31,22 @@ public sealed class SpawnPointSystem : EntitySystem if (_gameTicker.RunLevel == GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.LateJoin) { - args.SpawnResult = _stationSpawning.SpawnPlayerMob(xform.Coordinates, args.Job, - args.HumanoidCharacterProfile); + args.SpawnResult = _stationSpawning.SpawnPlayerMob( + xform.Coordinates, + args.Job, + args.HumanoidCharacterProfile, + args.Station); + 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); + args.SpawnResult = _stationSpawning.SpawnPlayerMob( + xform.Coordinates, + args.Job, + args.HumanoidCharacterProfile, + args.Station); + return; } } @@ -48,7 +56,12 @@ public sealed class SpawnPointSystem : EntitySystem foreach (var spawnPoint in points) { var xform = Transform(spawnPoint.Owner); - args.SpawnResult = _stationSpawning.SpawnPlayerMob(xform.Coordinates, args.Job, args.HumanoidCharacterProfile); + args.SpawnResult = _stationSpawning.SpawnPlayerMob( + xform.Coordinates, + args.Job, + args.HumanoidCharacterProfile, + args.Station); + return; } diff --git a/Content.Server/Station/Components/StationJobsComponent.cs b/Content.Server/Station/Components/StationJobsComponent.cs index 597d309f02..b73332783f 100644 --- a/Content.Server/Station/Components/StationJobsComponent.cs +++ b/Content.Server/Station/Components/StationJobsComponent.cs @@ -27,6 +27,11 @@ public sealed class StationJobsComponent : Component /// [DataField("totalJobs")] public int TotalJobs; + /// + /// Station is running on extended access. + /// + [DataField("extendedAccess")] public bool ExtendedAccess; + /// /// The percentage of jobs remaining. /// diff --git a/Content.Server/Station/StationConfig.Jobs.cs b/Content.Server/Station/StationConfig.Jobs.cs index d87fafb656..4aeb79a967 100644 --- a/Content.Server/Station/StationConfig.Jobs.cs +++ b/Content.Server/Station/StationConfig.Jobs.cs @@ -25,4 +25,14 @@ public sealed partial class StationConfig /// job name: [round-start, mid-round] /// public IReadOnlyDictionary> AvailableJobs => _availableJobs; + + /// + /// If there are less than or equal this amount of players in the game at round start, + /// people get extended access levels from job prototypes. + /// + /// + /// Set to -1 to disable extended access. + /// + [DataField("extendedAccessThreshold")] + public int ExtendedAccessThreshold { get; set; } = 15; } diff --git a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs index 41c481065f..ff78922267 100644 --- a/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs +++ b/Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration.Managers; +using Content.Server.Station.Components; using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Shared.Network; @@ -304,6 +305,24 @@ public sealed partial class StationJobsSystem } } + public void CalcExtendedAccess(Dictionary jobsCount) + { + // Calculate whether stations need to be on extended access or not. + foreach (var (station, count) in jobsCount) + { + var jobs = Comp(station); + var data = Comp(station); + + var thresh = data.StationConfig?.ExtendedAccessThreshold ?? -1; + + jobs.ExtendedAccess = count <= thresh; + + Logger.DebugS( + "station", "Station {Station} on extended access: {ExtendedAccess}", + Name(station), jobs.ExtendedAccess); + } + } + /// /// Gets all jobs that the input players have that match the given weight and priority. /// diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs index 08804b525a..fe12458ec3 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -7,6 +7,7 @@ using Content.Server.PDA; using Content.Server.Roles; using Content.Server.Station.Components; using Content.Shared.Access.Components; +using Content.Shared.Access.Systems; using Content.Shared.CCVar; using Content.Shared.Inventory; using Content.Shared.PDA; @@ -35,6 +36,7 @@ public sealed class StationSpawningSystem : EntitySystem [Dependency] private readonly IdCardSystem _cardSystem = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly PDASystem _pdaSystem = default!; + [Dependency] private readonly AccessSystem _accessSystem = default!; /// public override void Initialize() @@ -82,8 +84,13 @@ public sealed class StationSpawningSystem : EntitySystem /// Coordinates to spawn the character at. /// Job to assign to the character, if any. /// Appearance profile to use for the character. + /// The station this player is being spawned on. /// The spawned entity - public EntityUid SpawnPlayerMob(EntityCoordinates coordinates, Job? job, HumanoidCharacterProfile? profile) + public EntityUid SpawnPlayerMob( + EntityCoordinates coordinates, + Job? job, + HumanoidCharacterProfile? profile, + EntityUid? station) { var entity = EntityManager.SpawnEntity( _prototypeManager.Index(profile?.Species ?? SpeciesManager.DefaultSpecies).Prototype, @@ -94,7 +101,7 @@ public sealed class StationSpawningSystem : EntitySystem var startingGear = _prototypeManager.Index(job.StartingGear); EquipStartingGear(entity, startingGear, profile); if (profile != null) - EquipIdCard(entity, profile.Name, job.Prototype); + EquipIdCard(entity, profile.Name, job.Prototype, station); } if (profile != null) @@ -154,7 +161,8 @@ public sealed class StationSpawningSystem : EntitySystem /// Entity to load out. /// Character name to use for the ID. /// Job prototype to use for the PDA and ID. - public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype) + /// The station this player is being spawned on. + public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station) { if (!_inventorySystem.TryGetSlotEntity(entity, "id", out var idUid)) return; @@ -163,12 +171,19 @@ public sealed class StationSpawningSystem : EntitySystem return; var card = pdaComponent.ContainedID; - _cardSystem.TryChangeFullName(card.Owner, characterName, card); - _cardSystem.TryChangeJobTitle(card.Owner, jobPrototype.Name, card); + var cardId = card.Owner; + _cardSystem.TryChangeFullName(cardId, characterName, card); + _cardSystem.TryChangeJobTitle(cardId, jobPrototype.Name, card); + + var extendedAccess = false; + if (station != null) + { + var data = Comp(station.Value); + extendedAccess = data.ExtendedAccess; + } + + _accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess); - var access = EntityManager.GetComponent(card.Owner); - var accessTags = access.Tags; - accessTags.UnionWith(jobPrototype.Access); _pdaSystem.SetOwner(pdaComponent, characterName); } diff --git a/Content.Shared/Access/Systems/AccessSystem.cs b/Content.Shared/Access/Systems/AccessSystem.cs index 539e607efd..c1b2f02317 100644 --- a/Content.Shared/Access/Systems/AccessSystem.cs +++ b/Content.Shared/Access/Systems/AccessSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Access.Components; +using Content.Shared.Roles; using Robust.Shared.Prototypes; namespace Content.Shared.Access.Systems @@ -56,5 +57,33 @@ namespace Content.Shared.Access.Systems return true; } + + /// + /// Set the access on an to the access for a specific job. + /// + /// The ID of the entity with the access component. + /// The job prototype to use access from. + /// Whether to apply extended job access. + /// The access component. + public void SetAccessToJob( + EntityUid uid, + JobPrototype prototype, + bool extended, + AccessComponent? access = null) + { + if (!Resolve(uid, ref access)) + return; + + access.Tags.Clear(); + access.Tags.UnionWith(prototype.Access); + + TryAddGroups(uid, prototype.AccessGroups, access); + + if (extended) + { + access.Tags.UnionWith(prototype.ExtendedAccess); + TryAddGroups(uid, prototype.ExtendedAccessGroups, access); + } + } } } diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index 9951756075..e3697aff5c 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -61,5 +61,11 @@ namespace Content.Shared.Roles [DataField("accessGroups", customTypeSerializer: typeof(PrototypeIdListSerializer))] public IReadOnlyCollection AccessGroups { get; } = Array.Empty(); + + [DataField("extendedAccess", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public IReadOnlyCollection ExtendedAccess { get; } = Array.Empty(); + + [DataField("extendedAccessGroups", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public IReadOnlyCollection ExtendedAccessGroups { get; } = Array.Empty(); } } diff --git a/Resources/Locale/en-US/job/job.ftl b/Resources/Locale/en-US/job/job.ftl index 22b9179f52..c85e6a468f 100644 --- a/Resources/Locale/en-US/job/job.ftl +++ b/Resources/Locale/en-US/job/job.ftl @@ -3,3 +3,4 @@ job-greet-important-disconnect-admin-notify = You are playing a job that is impo job-greet-supervisors-warning = As the {$jobName} you answer directly to {$supervisors}. Special circumstances may change this. job-greet-join-notify-crew = { CAPITALIZE($jobName)} {$characterName} on deck! job-greet-join-notify-crew-announcer = Station +job-greet-crew-shortages = As this station was initially staffed with a skeleton crew, additional access has been added to your ID card. diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml index 7f3e83d1f1..25122269f9 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml @@ -10,6 +10,9 @@ - Service - Maintenance - Bar + extendedAccess: + - Kitchen + - Hydroponics - type: startingGear id: BartenderGear diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml b/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml index 29b4e9f80a..1d0e9c4653 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml @@ -10,6 +10,9 @@ - Service - Maintenance - Hydroponics + extendedAccess: + - Kitchen + - Bar - type: startingGear id: BotanistGear diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml index 9e94b06732..e274ce4c25 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml @@ -10,6 +10,9 @@ - Service - Maintenance - Kitchen + extendedAccess: + - Hydroponics + - Bar - type: startingGear id: ChefGear diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml index 3ba8b6c932..aa01045d6a 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml @@ -10,6 +10,8 @@ - Maintenance - Engineering - External + extendedAccess: + - Atmospherics - type: startingGear id: StationEngineerGear diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml index 4ab8218c61..4e6a7ccf51 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml @@ -9,6 +9,8 @@ access: - Medical - Maintenance + extendedAccess: + - Chemistry - type: startingGear id: DoctorGear