diff --git a/Content.Server/StationEvents/Components/GreytideVirusComponent.cs b/Content.Server/StationEvents/Components/GreytideVirusComponent.cs
new file mode 100644
index 000000000000..307f131db11d
--- /dev/null
+++ b/Content.Server/StationEvents/Components/GreytideVirusComponent.cs
@@ -0,0 +1,38 @@
+using Content.Server.StationEvents.Events;
+using Content.Shared.Access;
+using Content.Shared.Destructible.Thresholds;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.StationEvents.Components;
+
+///
+/// Greytide Virus event specific configuration
+///
+[RegisterComponent, Access(typeof(GreytideVirusRule))]
+public sealed partial class GreytideVirusRuleComponent : Component
+{
+ ///
+ /// Range from which the severity is randomly picked from.
+ ///
+ [DataField]
+ public MinMax SeverityRange = new(1, 3);
+
+ ///
+ /// Severity corresponding to the number of access groups affected.
+ /// Will pick randomly from the SeverityRange if not specified.
+ ///
+ [DataField]
+ public int? Severity;
+
+ ///
+ /// Access groups to pick from.
+ ///
+ [DataField]
+ public List> AccessGroups = new();
+
+ ///
+ /// Entities with this access level will be ignored.
+ ///
+ [DataField]
+ public List> Blacklist = new();
+}
diff --git a/Content.Server/StationEvents/Events/GreytideVirusRule.cs b/Content.Server/StationEvents/Events/GreytideVirusRule.cs
new file mode 100644
index 000000000000..f60d80ba9c57
--- /dev/null
+++ b/Content.Server/StationEvents/Events/GreytideVirusRule.cs
@@ -0,0 +1,96 @@
+using Content.Server.StationEvents.Components;
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using Content.Shared.Access.Components;
+using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
+using Content.Shared.Lock;
+using Content.Shared.GameTicking.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents.Events;
+
+
+///
+/// Greytide Virus event
+/// This will open and bolt airlocks and unlock lockers from randomly selected access groups.
+///
+public sealed class GreytideVirusRule : StationEventSystem
+{
+ [Dependency] private readonly AccessReaderSystem _access = default!;
+ [Dependency] private readonly SharedDoorSystem _door = default!;
+ [Dependency] private readonly LockSystem _lock = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Added(EntityUid uid, GreytideVirusRuleComponent virusComp, GameRuleComponent gameRule, GameRuleAddedEvent args)
+ {
+ if (!TryComp(uid, out var stationEvent))
+ return;
+
+ // pick severity randomly from range if not specified otherwise
+ virusComp.Severity ??= virusComp.SeverityRange.Next(_random);
+ virusComp.Severity = Math.Min(virusComp.Severity.Value, virusComp.AccessGroups.Count);
+
+ stationEvent.StartAnnouncement = Loc.GetString("station-event-greytide-virus-start-announcement", ("severity", virusComp.Severity.Value));
+ base.Added(uid, virusComp, gameRule, args);
+ }
+ protected override void Started(EntityUid uid, GreytideVirusRuleComponent virusComp, GameRuleComponent gameRule, GameRuleStartedEvent args)
+ {
+ base.Started(uid, virusComp, gameRule, args);
+
+ if (virusComp.Severity == null)
+ return;
+
+ // pick random access groups
+ var chosen = _random.GetItems(virusComp.AccessGroups, virusComp.Severity.Value, allowDuplicates: false);
+
+ // combine all the selected access groups
+ var accessIds = new HashSet>();
+ foreach (var group in chosen)
+ {
+ if (_prototype.TryIndex(group, out var proto))
+ accessIds.UnionWith(proto.Tags);
+ }
+
+ var firelockQuery = GetEntityQuery();
+ var accessQuery = GetEntityQuery();
+
+ var lockQuery = AllEntityQuery();
+ while (lockQuery.MoveNext(out var lockUid, out var lockComp))
+ {
+ if (!accessQuery.TryComp(lockUid, out var accessComp))
+ continue;
+
+ // check access
+ // the AreAccessTagsAllowed function is a little weird because it technically has support for certain tags to be locked out of opening something
+ // which might have unintened side effects (see the comments in the function itself)
+ // but no one uses that yet, so it is fine for now
+ if (!_access.AreAccessTagsAllowed(accessIds, accessComp) || _access.AreAccessTagsAllowed(virusComp.Blacklist, accessComp))
+ continue;
+
+ // open lockers
+ _lock.Unlock(lockUid, null, lockComp);
+ }
+
+ var airlockQuery = AllEntityQuery();
+ while (airlockQuery.MoveNext(out var airlockUid, out var airlockComp, out var doorComp))
+ {
+ // don't space everything
+ if (firelockQuery.HasComp(airlockUid))
+ continue;
+
+ // use the access reader from the door electronics if they exist
+ if (!_access.GetMainAccessReader(airlockUid, out var accessComp))
+ continue;
+
+ // check access
+ if (!_access.AreAccessTagsAllowed(accessIds, accessComp) || _access.AreAccessTagsAllowed(virusComp.Blacklist, accessComp))
+ continue;
+
+ // open and bolt airlocks
+ _door.TryOpenAndBolt(airlockUid, doorComp, airlockComp);
+ }
+ }
+}
diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs
index 835adb31c05b..69905d1bd6b6 100644
--- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs
+++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs
@@ -396,6 +396,25 @@ public void OnPartialOpen(EntityUid uid, DoorComponent? door = null)
Dirty(uid, door);
}
+
+ ///
+ /// Opens and then bolts a door.
+ /// Different from emagging this does not remove the access reader, so it can be repaired by simply unbolting the door.
+ ///
+ public bool TryOpenAndBolt(EntityUid uid, DoorComponent? door = null, AirlockComponent? airlock = null)
+ {
+ if (!Resolve(uid, ref door, ref airlock))
+ return false;
+
+ if (IsBolted(uid) || !airlock.Powered || door.State != DoorState.Closed)
+ {
+ return false;
+ }
+
+ SetState(uid, DoorState.Emagging, door);
+
+ return true;
+ }
#endregion
#region Closing
diff --git a/Resources/Locale/en-US/station-events/events/greytide-virus.ftl b/Resources/Locale/en-US/station-events/events/greytide-virus.ftl
new file mode 100644
index 000000000000..7e6f5e32ca6b
--- /dev/null
+++ b/Resources/Locale/en-US/station-events/events/greytide-virus.ftl
@@ -0,0 +1 @@
+station-event-greytide-virus-start-announcement = Gr3y.T1d3 virus detected in the station's secure locking encryption subroutines. Severity level of { $severity }. Recommend station AI involvement.
diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml
index 08218accede4..98b6690ebb42 100644
--- a/Resources/Prototypes/GameRules/events.yml
+++ b/Resources/Prototypes/GameRules/events.yml
@@ -10,6 +10,7 @@
- id: ClericalError
- id: CockroachMigration
- id: GasLeak
+ - id: GreytideVirus
- id: IonStorm # its calm like 90% of the time smh
- id: KudzuGrowth
- id: MassHallucinations
@@ -540,3 +541,24 @@
maxOccurrences: 1 # this event has diminishing returns on interesting-ness, so we cap it
weight: 5
- type: MobReplacementRule
+
+- type: entity
+ id: GreytideVirus
+ parent: BaseStationEventShortDelay
+ components:
+ - type: StationEvent
+ startAudio:
+ path: /Audio/Announcements/attention.ogg
+ weight: 5
+ minimumPlayers: 25
+ reoccurrenceDelay: 20
+ - type: GreytideVirusRule
+ accessGroups:
+ - Cargo
+ - Command
+ - Engineering
+ - Research
+ - Security
+ - Service
+ blacklist:
+ - External # don't space everything