fix some station events targeting off-station grids (#45813)

* fix vent critters spawning off station

* I polish the code

* add utility to get entities ONLY on the station. use in vent critters rule.

* fix station events affecting outside grids

* better argument names for GetEntitiesWithComponentOnStation

* BreakerFlipRule

* \n

* fix doc in GetEntitiesWithComponentOnStation

* keep the comments

* remove BreakerFlipRuleComponent Blacklist

* minor cleanup

* review

* format

---------

Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
This commit is contained in:
qwerltaz 2026-10-03 19:17:59 +00:00 • committed by GitHub
parent 29668eb5a3
commit e39cd127d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 319 additions and 424 deletions

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Atmos.EntitySystems;
@ -311,6 +310,53 @@ public sealed partial class ServerStationSystem : Shared.Station.Systems.Station
QueueDel(station);
}
/// <summary>
/// Get entities with <see cref="TComponent"/> on a random event-eligible station's largest grid.
/// </summary>
/// <param name="onlyAnchored">Whether to only get anchored entities.
/// Good check for air vents, bad for containers like crates.</param>
/// <returns>All matching entities.</returns>
public HashSet<Entity<TComponent>> GetEntitiesWithComponentOnStation<TComponent>(bool onlyAnchored)
where TComponent : IComponent
{
return GetEntitiesWithComponentOnStation<TComponent>(onlyAnchored, out _);
}
/// <param name="station">The station picked to search for the entities.</param>
/// <inheritdoc cref="GetEntitiesWithComponentOnStation{TComponent}(bool)" />
public HashSet<Entity<TComponent>> GetEntitiesWithComponentOnStation<TComponent>(bool onlyAnchored,
out Entity<StationDataComponent>? station)
where TComponent : IComponent
{
HashSet<Entity<TComponent>> entities = [];
if (!TryGetRandomStation<StationEventEligibleComponent>(out var eligibleStation))
{
station = null;
return entities;
}
station = (eligibleStation.Value.Owner, eligibleStation.Value.Comp1);
var grid = GetLargestGrid(station.Value.Owner);
if (grid is null)
return entities;
var locations = EntityQueryEnumerator<TComponent, TransformComponent>();
while (locations.MoveNext(out var uid, out var component, out var transform))
{
if (onlyAnchored && !transform.Anchored)
continue;
if (transform.GridUid != grid)
continue;
entities.Add((uid, component));
}
return entities;
}
/// <inheritdoc/>
public override bool TryFindRandomTileOnStation(Entity<StationDataComponent> station,
out Vector2i tile,
@ -319,89 +365,40 @@ public sealed partial class ServerStationSystem : Shared.Station.Systems.Station
int numAttempts = 10)
{
tile = default;
targetCoords = EntityCoordinates.Invalid;
targetGrid = null;
targetCoords = EntityCoordinates.Invalid;
// Weight grid choice by tilecount
var totalTiles = 0;
var grids = new List<(Entity<MapGridComponent> Entity, int Count, List<TileRef> Tiles)>();
foreach (var possibleTarget in station.Comp.Grids)
{
if (!GridQuery.TryComp(possibleTarget, out var comp))
continue;
// Get the tile count for the given grid.
var tileCount = Map.GetFilledTileCount((possibleTarget, comp));
// Just to be sure, no empty elements.
if (tileCount > 0)
{
grids.Add(((possibleTarget, comp), tileCount, new()));
totalTiles += tileCount;
}
}
if (grids.Count == 0)
var targetGridMaybe = GetLargestGrid(station.Owner);
if (targetGridMaybe is null)
return false;
for (var i = 0; i < numAttempts; i++)
if (!TryComp<MapGridComponent>(targetGridMaybe.Value, out var comp))
return false;
targetGrid = (targetGridMaybe.Value, comp);
var grid = targetGrid.Value;
var gridTiles = Map.GetAllTiles(grid, grid.Comp).ToList();
var totalTiles = gridTiles.Count;
if (totalTiles == 0)
return false;
for (var i = 0; i < numAttempts && totalTiles > 0; i++)
{
// Find random tile within list.
var nextTileIndex = Random.Next(totalTiles);
TileRef? randomTileRef = null;
var startIndex = 0;
for (int j = 0; j < grids.Count; j++)
{
var grid = grids[j];
// If the index is in this particular grid, find it and remove the tile to prevent selecting it twice.
if (nextTileIndex >= startIndex + grid.Count)
{
startIndex += grid.Count;
continue;
}
targetGrid = grid.Entity;
// Empty list: hasn't been queried yet - get our tiles.
if (grid.Tiles.Count <= 0)
{
grid.Tiles = Map.GetAllTiles(targetGrid.Value, targetGrid.Value.Comp).ToList();
// Actual list count doesn't match expected count (a bug - return failure).
Debug.Assert(grid.Tiles.Count == grid.Count);
if (grid.Tiles.Count != grid.Count)
return false;
}
var ourTileIndex = nextTileIndex - startIndex;
randomTileRef = grid.Tiles[ourTileIndex];
grid.Tiles.RemoveSwap(ourTileIndex);
grid.Count--;
totalTiles--;
// Empty list, remove element
if (grid.Tiles.Count <= 0)
grids.RemoveSwap(j);
break;
}
// Out of valid tiles, return early.
if (randomTileRef is not { } tileRef)
return false;
// Could not find a grid...
if (targetGrid == null)
continue;
var tileRef = gridTiles[nextTileIndex];
gridTiles.RemoveSwap(nextTileIndex);
totalTiles--;
// Invalid tile, try again.
if (_atmos.IsTileSpace(targetGrid.Value.Owner, Transform(targetGrid.Value).MapUid, tileRef.GridIndices)
|| _atmos.IsTileAirBlockedCached(targetGrid.Value.Owner, tileRef.GridIndices))
if (_atmos.IsTileSpace(grid.Owner, Transform(grid).MapUid, tileRef.GridIndices)
|| _atmos.IsTileAirBlockedCached(grid.Owner, tileRef.GridIndices))
{
continue;
}
targetCoords = Map.GridTileToLocal(targetGrid.Value, targetGrid.Value, tileRef.GridIndices);
targetCoords = Map.GridTileToLocal(grid, grid.Comp, tileRef.GridIndices);
tile = tileRef.GridIndices;
return true;
}
@ -415,92 +412,67 @@ public sealed partial class ServerStationSystem : Shared.Station.Systems.Station
/// This is the ideal point to add components to it.
/// </summary>
[PublicAPI]
public sealed class StationInitializedEvent : EntityEventArgs
public sealed class StationInitializedEvent(EntityUid station) : EntityEventArgs
{
/// <summary>
/// Station this event is for.
/// </summary>
public EntityUid Station;
public StationInitializedEvent(EntityUid station)
{
Station = station;
}
public EntityUid 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
public sealed class StationGridAddedEvent(EntityUid gridId, EntityUid station, bool isSetup) : EntityEventArgs
{
/// <summary>
/// ID of the grid added to the station.
/// </summary>
public EntityUid GridId;
public EntityUid GridId = gridId;
/// <summary>
/// EntityUid of the station this grid was added to.
/// </summary>
public EntityUid Station;
public EntityUid Station = station;
/// <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(EntityUid gridId, EntityUid station, bool isSetup)
{
GridId = gridId;
Station = station;
IsSetup = isSetup;
}
public bool 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
public sealed class StationGridRemovedEvent(EntityUid gridId, EntityUid station) : EntityEventArgs
{
/// <summary>
/// ID of the grid removed from the station.
/// </summary>
public EntityUid GridId;
public EntityUid GridId = gridId;
/// <summary>
/// EntityUid of the station this grid was added to.
/// </summary>
public EntityUid Station;
public StationGridRemovedEvent(EntityUid gridId, EntityUid station)
{
GridId = gridId;
Station = station;
}
public EntityUid Station = station;
}
/// <summary>
/// Directed event fired on a station when it is renamed.
/// </summary>
[PublicAPI]
public sealed class StationRenamedEvent : EntityEventArgs
public sealed class StationRenamedEvent(string oldName, string newName) : EntityEventArgs
{
/// <summary>
/// Prior name of the station.
/// </summary>
public string OldName;
public string OldName = oldName;
/// <summary>
/// New name of the station.
/// </summary>
public string NewName;
public StationRenamedEvent(string oldName, string newName)
{
OldName = oldName;
NewName = newName;
}
public string NewName = newName;
}

View file

@ -4,11 +4,4 @@ using Content.Shared.Whitelist;
namespace Content.Server.StationEvents.Components;
[RegisterComponent, Access(typeof(BreakerFlipRule))]
public sealed partial class BreakerFlipRuleComponent : Component
{
/// <summary>
/// Blacklist of structures not eligible to trigger this game rule.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}
public sealed partial class BreakerFlipRuleComponent : Component;

View file

@ -32,24 +32,12 @@ public sealed partial class GasLeakRuleComponent : Component
[DataField]
public float LeakCooldown = 1.0f;
/// <summary>
/// The station where the leak is located.
/// </summary>
[DataField]
public EntityUid TargetStation;
/// <summary>
/// The specific grid where the leak is located.
/// </summary>
[DataField]
public EntityUid TargetGrid;
/// <summary>
/// The tile coordinates where the leak is located.
/// </summary>
[DataField]
public Vector2i TargetTile;
/// <summary>
/// The world coordinates of the leak location.
/// </summary>

View file

@ -1,4 +1,6 @@
using Content.Server.StationEvents.Events;
using Content.Shared.Doors.Components;
using Content.Shared.Light.Components;
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
@ -40,6 +42,18 @@ public sealed partial class SolarFlareRuleComponent : Component
[DataField("extraCount")]
public uint ExtraCount;
/// <summary>
/// The collection of lights that will be affected by the solar flare event.
/// </summary>
[DataField]
public HashSet<(EntityUid, PoweredLightComponent)> AffectedLights = [];
/// <summary>
/// The collection of airlocks that will be affected by the solar flare event.
/// </summary>
[DataField]
public HashSet<(EntityUid, AirlockComponent)> AffectedAirlocks = [];
/// <summary>
/// Chance light bulb breaks per second during event
/// </summary>

View file

@ -1,10 +1,9 @@
using System.Linq;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.StationEvents.Components;
using Content.Shared.Database;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
namespace Content.Server.StationEvents.Events;
@ -17,7 +16,6 @@ namespace Content.Server.StationEvents.Events;
public sealed partial class BreakerFlipRule : StationEventSystem<BreakerFlipRuleComponent>
{
[Dependency] private ApcSystem _apcSystem = default!;
[Dependency] private EntityWhitelistSystem _whitelist = default!;
protected override void Added(Entity<BreakerFlipRuleComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
@ -34,20 +32,7 @@ public sealed partial class BreakerFlipRule : StationEventSystem<BreakerFlipRule
{
base.Started(ent, ref args);
if (!Station.TryGetRandomStation<StationEventEligibleComponent>(
out var chosenStation,
uid => _whitelist.IsWhitelistFailOrNull(ent.Comp1.Blacklist, uid)))
return;
var stationApcs = new List<Entity<ApcComponent>>();
var query = EntityQueryEnumerator<ApcComponent, TransformComponent>();
while (query.MoveNext(out var apcUid, out var apc, out var xform))
{
if (apc.MainBreakerEnabled && CompOrNull<StationMemberComponent>(xform.GridUid)?.Station == chosenStation.Value.Owner)
{
stationApcs.Add((apcUid, apc));
}
}
var stationApcs = Station.GetEntitiesWithComponentOnStation<ApcComponent>(true).ToList();
var toDisable = Math.Min(RobustRandom.Next(3, 7), stationApcs.Count);
if (toDisable == 0)
@ -61,7 +46,8 @@ public sealed partial class BreakerFlipRule : StationEventSystem<BreakerFlipRule
_apcSystem.ApcToggleBreaker(apc, apc);
var stateString = apc.Comp.MainBreakerEnabled ? "Enabled" : "Disabled";
AdminLogManager.Add(LogType.ItemConfigure, LogImpact.Medium,
AdminLogManager.Add(LogType.ItemConfigure,
LogImpact.Medium,
$"Station event {ToPrettyString(ent):user} set the main breaker state of {ToPrettyString(apc):entity} to {stateString:state}");
}
}

View file

@ -1,4 +1,5 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Audio;
@ -26,20 +27,30 @@ public sealed partial class GasLeakRule : StationEventSystem<GasLeakRuleComponen
if (!TryComp<StationEventComponent>(ent, out var stationEvent))
return;
// Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there.
if (Station.TryFindRandomTile(out gasLeak.TargetTile, out var target, out var grid, out gasLeak.TargetCoords))
var stationVents = Station.GetEntitiesWithComponentOnStation<GasVentScrubberComponent>(true);
if (stationVents.Count == 0)
{
gasLeak.TargetGrid = grid.Value;
gasLeak.TargetStation = target.Value;
gasLeak.FoundTile = true;
ForceEndSelf((ent, gameRule));
return;
}
gasLeak.LeakGas = RobustRandom.Pick(gasLeak.LeakableGases);
// Was 50-50 on using normal distribution.
var totalGas = RobustRandom.Next(gasLeak.MinimumGas, gasLeak.MaximumGas);
gasLeak.MolesPerSecond = RobustRandom.Next(gasLeak.MinimumMolesPerSecond, gasLeak.MaximumMolesPerSecond);
var targetVent = RobustRandom.Pick(stationVents);
if (gameRule.Delay is {} startAfter)
stationEvent.EndTime = _timing.CurTime + TimeSpan.FromSeconds(totalGas / gasLeak.MolesPerSecond + startAfter.Next(RobustRandom));
gasLeak.FoundTile = true;
gasLeak.TargetCoords = Transform(targetVent).Coordinates;
gasLeak.TargetGrid = Transform(targetVent).GridUid!.Value;
// Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there.
gasLeak.LeakGas = RobustRandom.Pick(gasLeak.LeakableGases);
// Was 50-50 on using normal distribution.
var totalGas = RobustRandom.Next(gasLeak.MinimumGas, gasLeak.MaximumGas);
gasLeak.MolesPerSecond = RobustRandom.Next(gasLeak.MinimumMolesPerSecond, gasLeak.MaximumMolesPerSecond);
if (gameRule.Delay is { } startAfter)
{
stationEvent.EndTime = _timing.CurTime +
TimeSpan.FromSeconds(totalGas / gasLeak.MolesPerSecond +
startAfter.Next(RobustRandom));
}
// Look technically if you wanted to guarantee a leak you'd do this in announcement but having the announcement
@ -64,7 +75,7 @@ public sealed partial class GasLeakRule : StationEventSystem<GasLeakRuleComponen
return;
}
var environment = _atmosphere.GetTileMixture(component.TargetGrid, null, component.TargetTile, true);
var environment = _atmosphere.GetTileMixture(component.TargetGrid, null, (Vector2i)component.TargetCoords.Position, true);
environment?.AdjustMoles(component.LeakGas, component.LeakCooldown * component.MolesPerSecond);
}
@ -77,20 +88,20 @@ public sealed partial class GasLeakRule : StationEventSystem<GasLeakRuleComponen
private void Spark(Entity<GasLeakRuleComponent> rule)
{
if (RobustRandom.NextFloat() <= rule.Comp.SparkChance)
{
if (!rule.Comp.FoundTile ||
rule.Comp.TargetGrid == default ||
(!Exists(rule.Comp.TargetGrid) ? EntityLifeStage.Deleted : MetaData(rule.Comp.TargetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
!_atmosphere.IsSimulatedGrid(rule.Comp.TargetGrid))
{
return;
}
if (!(RobustRandom.NextFloat() <= rule.Comp.SparkChance))
return;
// Don't want it to be so obnoxious as to instantly murder anyone in the area but enough that
// it COULD start potentially start a bigger fire.
_atmosphere.HotspotExpose(rule.Comp.TargetGrid, rule.Comp.TargetTile, 700f, 50f, null, true);
Audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"), rule.Comp.TargetCoords);
if (!rule.Comp.FoundTile ||
rule.Comp.TargetGrid == default ||
(!Exists(rule.Comp.TargetGrid) ? EntityLifeStage.Deleted : MetaData(rule.Comp.TargetGrid).EntityLifeStage) >= EntityLifeStage.Deleted ||
!_atmosphere.IsSimulatedGrid(rule.Comp.TargetGrid))
{
return;
}
// Don't want it to be so obnoxious as to instantly murder anyone in the area but enough that
// it COULD start potentially start a bigger fire.
_atmosphere.HotspotExpose(rule.Comp.TargetGrid, (Vector2i)rule.Comp.TargetCoords.Position, 700f, 50f, null, true);
Audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"), rule.Comp.TargetCoords);
}
}

View file

@ -9,174 +9,160 @@ using Robust.Shared.Player;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.StationEvents.Events
{
/// <summary>
/// Handler for events that force a number of APCs off on a station for a period of time.
/// </summary>
/// <seealso cref="PowerGridCheckRuleComponent"/>
[UsedImplicitly]
public sealed partial class PowerGridCheckRule : StationEventSystem<PowerGridCheckRuleComponent>
{
[Dependency] private ApcSystem _apcSystem = default!;
namespace Content.Server.StationEvents.Events;
public override void Initialize()
/// <summary>
/// Handler for events that force a number of APCs off on a station for a period of time.
/// </summary>
/// <seealso cref="PowerGridCheckRuleComponent"/>
[UsedImplicitly]
public sealed partial class PowerGridCheckRule : StationEventSystem<PowerGridCheckRuleComponent>
{
[Dependency] private ApcSystem _apc = default!;
protected override void Started(Entity<PowerGridCheckRuleComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
{
base.Started(ent, ref args);
var apcs = Station.GetEntitiesWithComponentOnStation<ApcComponent>(true, out var chosenStation);
if (chosenStation is null)
return;
var powerGridCheck = ent.Comp1;
powerGridCheck.AffectedStation = chosenStation.Value;
var largestGrid = Station.GetLargestGrid(chosenStation.Value.AsNullable());
if (largestGrid is null)
return;
foreach (var apcUid in apcs)
{
base.Initialize();
SubscribeLocalEvent<PowerGridCheckNotifyComponent, ComponentStartup>(OnApcStartup);
SubscribeLocalEvent<PowerGridCheckNotifyComponent, ApcToggleMainBreakerAttemptEvent>(OnApcToggleMainBreaker);
if (!apcUid.Comp.MainBreakerEnabled)
continue;
var apcTransform = Transform(apcUid);
if (apcTransform.GridUid != largestGrid)
continue;
powerGridCheck.Powered.Add(apcUid);
}
protected override void Started(Entity<PowerGridCheckRuleComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
RobustRandom.Shuffle(powerGridCheck.Powered);
powerGridCheck.NumberPerSecond = Math.Max(1, (int)(powerGridCheck.Powered.Count / powerGridCheck.SecondsUntilOff)); // Number of APCs to turn off every second. At least one.
}
/// <summary>
/// Check if the entity should be affected by an existing
/// PowerGridCheckRuleComponent and if so, turns off the APC.
/// </summary>
[SubscribeLocalEvent]
private void OnApcStartup(EntityUid apcUid, PowerGridCheckNotifyComponent comp, ComponentStartup args)
{
if (!TryComp<ApcComponent>(apcUid, out var apcComp))
return;
var rule = GetRuleAffectingEntity(apcUid);
if (rule == null || !apcComp.MainBreakerEnabled)
return;
_apc.ApcToggleBreaker(apcUid, apcComp);
rule.Unpowered.Add(apcUid);
}
[SubscribeLocalEvent]
private void OnApcToggleMainBreaker(EntityUid uid, PowerGridCheckNotifyComponent component, ref ApcToggleMainBreakerAttemptEvent args)
{
args.Cancelled |= GetRuleAffectingEntity(uid) != null;
}
/// <summary>
/// Returns the PowerGridCheckRuleComponent affecting the uid, or null if none
/// </summary>
private PowerGridCheckRuleComponent? GetRuleAffectingEntity(EntityUid uid)
{
if (!TryComp(uid, out TransformComponent? xform))
return null;
if (!TryComp<StationMemberComponent>(xform.GridUid, out var stationMemberComp))
return null;
var activeRules = AllEntityQuery<PowerGridCheckRuleComponent, ActiveGameRuleComponent>();
while (activeRules.MoveNext(out _, out var powerGridRule, out _))
{
base.Started(ent, ref args);
if (stationMemberComp.Station != powerGridRule.AffectedStation)
continue;
if (!Station.TryGetRandomStation<StationEventEligibleComponent>(out var chosenStation))
return;
var powerGridCheck = ent.Comp1;
powerGridCheck.AffectedStation = chosenStation.Value;
var largestGrid = Station.GetLargestGrid(chosenStation.Value.AsNullable());
var largestGrid = Station.GetLargestGrid(powerGridRule.AffectedStation);
if (largestGrid == null)
return;
continue;
var query = AllEntityQuery<ApcComponent, TransformComponent>();
while (query.MoveNext(out var apcUid, out var apc, out var transform))
{
if (!apc.MainBreakerEnabled)
continue;
if (xform.GridUid != largestGrid.Value)
continue;
if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station != chosenStation.Value.Owner)
continue;
if (transform.GridUid != largestGrid.Value)
continue;
powerGridCheck.Powered.Add(apcUid);
}
RobustRandom.Shuffle(powerGridCheck.Powered);
powerGridCheck.NumberPerSecond = Math.Max(1, (int)(powerGridCheck.Powered.Count / powerGridCheck.SecondsUntilOff)); // Number of APCs to turn off every second. At least one.
return powerGridRule;
}
/// <summary>
/// Check if the entity should be affected by an existing
/// PowerGridCheckRuleComponent and if so, turns off the APC.
/// </summary>
private void OnApcStartup(EntityUid apcUid, PowerGridCheckNotifyComponent comp, ComponentStartup args)
{
if (!TryComp<ApcComponent>(apcUid, out var apcComp))
{
return;
}
return null;
}
PowerGridCheckRuleComponent? rule = GetRuleAffectingEntity(apcUid);
if (rule != null && apcComp.MainBreakerEnabled)
{
_apcSystem.ApcToggleBreaker(apcUid, apcComp);
rule.Unpowered.Add(apcUid);
}
protected override void Ended(Entity<PowerGridCheckRuleComponent> rule, ref GameRuleEndedEvent args)
{
base.Ended(rule, ref args);
foreach (var entity in rule.Comp.Unpowered)
{
if (Deleted(entity))
continue;
if (!TryComp<ApcComponent>(entity, out var apcComponent))
continue;
if (!apcComponent.MainBreakerEnabled)
_apc.ApcToggleBreaker(entity, apcComponent);
}
private void OnApcToggleMainBreaker(EntityUid uid, PowerGridCheckNotifyComponent component, ref ApcToggleMainBreakerAttemptEvent args)
// Can't use the default EndAudio
rule.Comp.AnnounceCancelToken?.Cancel();
rule.Comp.AnnounceCancelToken = new CancellationTokenSource();
Timer.Spawn(3000,
() =>
{
args.Cancelled |= GetRuleAffectingEntity(uid) != null;
Audio.PlayGlobal(rule.Comp.PowerOnSound, Filter.Broadcast(), true);
},
rule.Comp.AnnounceCancelToken.Token);
rule.Comp.Unpowered.Clear();
}
protected override void ActiveTick(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
var updates = 0;
component.FrameTimeAccumulator += frameTime;
if (component.FrameTimeAccumulator > component.UpdateRate)
{
updates = (int)(component.FrameTimeAccumulator / component.UpdateRate);
component.FrameTimeAccumulator -= component.UpdateRate * updates;
}
/// <summary>
/// Returns the PowerGridCheckRuleComponent affecting the uid, or null if none
/// </summary>
private PowerGridCheckRuleComponent? GetRuleAffectingEntity(EntityUid uid)
for (var i = 0; i < updates; i++)
{
if (!TryComp(uid, out TransformComponent? xform))
if (component.Powered.Count == 0)
break;
var selected = component.Powered.Pop();
if (Deleted(selected))
continue;
if (TryComp<ApcComponent>(selected, out var apcComponent))
{
return null;
if (apcComponent.MainBreakerEnabled)
_apc.ApcToggleBreaker(selected, apcComponent);
}
if (!TryComp<StationMemberComponent>(xform.GridUid, out var stationMemberComp))
{
return null;
}
var activeRules = AllEntityQuery<PowerGridCheckRuleComponent, ActiveGameRuleComponent>();
while (activeRules.MoveNext(out _, out var powerGridRule, out _))
{
if (stationMemberComp.Station != powerGridRule.AffectedStation)
continue;
var largestGrid = Station.GetLargestGrid(powerGridRule.AffectedStation);
if (largestGrid == null)
continue;
if (xform.GridUid != largestGrid.Value)
continue;
return powerGridRule;
}
return null;
}
protected override void Ended(Entity<PowerGridCheckRuleComponent> rule, ref GameRuleEndedEvent args)
{
base.Ended(rule, ref args);
foreach (var entity in rule.Comp.Unpowered)
{
if (Deleted(entity))
continue;
if (TryComp(entity, out ApcComponent? apcComponent))
{
if (!apcComponent.MainBreakerEnabled)
_apcSystem.ApcToggleBreaker(entity, apcComponent);
}
}
// Can't use the default EndAudio
rule.Comp.AnnounceCancelToken?.Cancel();
rule.Comp.AnnounceCancelToken = new CancellationTokenSource();
Timer.Spawn(3000,
() =>
{
Audio.PlayGlobal(rule.Comp.PowerOnSound, Filter.Broadcast(), true);
},
rule.Comp.AnnounceCancelToken.Token);
rule.Comp.Unpowered.Clear();
}
protected override void ActiveTick(EntityUid uid, PowerGridCheckRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
var updates = 0;
component.FrameTimeAccumulator += frameTime;
if (component.FrameTimeAccumulator > component.UpdateRate)
{
updates = (int)(component.FrameTimeAccumulator / component.UpdateRate);
component.FrameTimeAccumulator -= component.UpdateRate * updates;
}
for (var i = 0; i < updates; i++)
{
if (component.Powered.Count == 0)
break;
var selected = component.Powered.Pop();
if (Deleted(selected))
continue;
if (TryComp<ApcComponent>(selected, out var apcComponent))
{
if (apcComponent.MainBreakerEnabled)
_apcSystem.ApcToggleBreaker(selected, apcComponent);
}
component.Unpowered.Add(selected);
}
component.Unpowered.Add(selected);
}
}
}

View file

@ -1,7 +1,6 @@
using Content.Server.StationEvents.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Shared.Storage.Components;
using Robust.Shared.Map;
using Robust.Shared.Random;
@ -20,24 +19,17 @@ public sealed partial class RandomEntityStorageSpawnRule : StationEventSystem<Ra
{
base.Started(ent, ref args);
if (!Station.TryGetRandomStation<StationEventEligibleComponent>(out var station))
return;
var spawnRule = ent.Comp1;
var validLockers = new List<(EntityUid, EntityStorageComponent)>();
var validLockers = new List<Entity<EntityStorageComponent>>();
var spawn = Spawn(spawnRule.Prototype, MapCoordinates.Nullspace);
var query = EntityQueryEnumerator<EntityStorageComponent, TransformComponent>();
while (query.MoveNext(out var storageUid, out var storage, out var xform))
foreach (var storageEnt in Station.GetEntitiesWithComponentOnStation<EntityStorageComponent>(false))
{
if (Station.GetOwningStation(storageUid, xform) != station.Value.Owner)
if (!_entityStorage.CanInsert(spawn, storageEnt, storageEnt.Comp))
continue;
if (!_entityStorage.CanInsert(spawn, storageUid, storage))
continue;
validLockers.Add((storageUid, storage));
validLockers.Add(storageEnt);
}
if (validLockers.Count == 0)
@ -46,10 +38,8 @@ public sealed partial class RandomEntityStorageSpawnRule : StationEventSystem<Ra
return;
}
var (locker, storageComp) = RobustRandom.Pick(validLockers);
if (!_entityStorage.Insert(spawn, locker, storageComp))
{
var locker = RobustRandom.Pick(validLockers);
if (!_entityStorage.Insert(spawn, locker, locker.Comp))
Del(spawn);
}
}
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Light.EntitySystems;
using Content.Server.StationEvents.Components;
using Content.Shared.Doors.Components;
@ -25,7 +26,7 @@ public sealed partial class SolarFlareRule : StationEventSystem<SolarFlareRuleCo
[Dependency] private EntityQuery<HeadsetComponent> _headsetQuery;
private float _effectTimer = 0;
private float _effectTimer;
protected override void Started(Entity<SolarFlareRuleComponent, GameRuleComponent> ent, ref GameRuleStartedEvent args)
{
@ -36,6 +37,13 @@ public sealed partial class SolarFlareRule : StationEventSystem<SolarFlareRuleCo
var channel = RobustRandom.Pick(ent.Comp1.ExtraChannels);
ent.Comp1.AffectedChannels.Add(channel);
}
ent.Comp1.AffectedLights = Station.GetEntitiesWithComponentOnStation<PoweredLightComponent>(true)
.Select(e => (e.Owner, e.Comp))
.ToHashSet();
ent.Comp1.AffectedAirlocks = Station.GetEntitiesWithComponentOnStation<AirlockComponent>(true)
.Select(e => (e.Owner, e.Comp))
.ToHashSet();
}
protected override void ActiveTick(EntityUid uid, SolarFlareRuleComponent component, GameRuleComponent gameRule, float frameTime)
@ -43,21 +51,20 @@ public sealed partial class SolarFlareRule : StationEventSystem<SolarFlareRuleCo
base.ActiveTick(uid, component, gameRule, frameTime);
_effectTimer -= frameTime;
if (_effectTimer < 0)
if (!(_effectTimer < 0))
return;
_effectTimer += 1;
foreach (var light in component.AffectedLights)
{
_effectTimer += 1;
var lightQuery = EntityQueryEnumerator<PoweredLightComponent>();
while (lightQuery.MoveNext(out var lightEnt, out var light))
{
if (RobustRandom.Prob(component.LightBreakChancePerSecond))
_poweredLight.TryDestroyBulb(lightEnt, light);
}
var airlockQuery = EntityQueryEnumerator<AirlockComponent, DoorComponent>();
while (airlockQuery.MoveNext(out var airlockEnt, out var airlock, out var door))
{
if (airlock.AutoClose && RobustRandom.Prob(component.DoorToggleChancePerSecond))
_door.TryToggleDoor(airlockEnt, door);
}
if (RobustRandom.Prob(component.LightBreakChancePerSecond))
_poweredLight.TryDestroyBulb(light.Item1, light.Item2);
}
foreach (var airlockEnt in component.AffectedAirlocks)
{
if (airlockEnt.Item2.AutoClose && RobustRandom.Prob(component.DoorToggleChancePerSecond))
_door.TryToggleDoor(airlockEnt.Item1);
}
}

View file

@ -6,7 +6,6 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@ -25,9 +24,6 @@ public sealed partial class VentClogRule : StationEventSystem<VentClogRuleCompon
{
base.Started(ent, ref args);
if (!Station.TryGetRandomStation<StationEventEligibleComponent>(out var chosenStation))
return;
var ventClog = ent.Comp1;
// TODO: "safe random" for chems. Right now this includes admin chemicals.
@ -35,13 +31,9 @@ public sealed partial class VentClogRule : StationEventSystem<VentClogRuleCompon
.Where(x => !x.Abstract)
.Select(x => new ProtoId<ReagentPrototype>(x.ID)).ToList();
foreach (var (_, transform) in EntityQuery<GasVentPumpComponent, TransformComponent>())
foreach (var ventPump in Station.GetEntitiesWithComponentOnStation<GasVentPumpComponent>(true))
{
if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station != chosenStation.Value.Owner)
{
continue;
}
var targetCoords = Transform(ventPump).Coordinates;
var solution = new Solution();
if (!RobustRandom.Prob(0.33f))
@ -54,10 +46,10 @@ public sealed partial class VentClogRule : StationEventSystem<VentClogRuleCompon
var quantity = weak ? ventClog.WeakReagentQuantity : ventClog.ReagentQuantity;
solution.AddReagent(reagent, quantity);
var foamEnt = Spawn(ChemicalReactionSystem.FoamReaction, transform.Coordinates);
var foamEnt = Spawn(ChemicalReactionSystem.FoamReaction, targetCoords);
var spreadAmount = weak ? ventClog.WeakSpread : ventClog.Spread;
_smoke.StartSmoke(foamEnt, solution, ventClog.Time, spreadAmount);
Audio.PlayPvs(ventClog.Sound, transform.Coordinates);
Audio.PlayPvs(ventClog.Sound, targetCoords);
}
}
}

View file

@ -1,8 +1,6 @@
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Shared.Storage;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
@ -19,45 +17,31 @@ public sealed partial class VentCrittersRule : StationEventSystem<VentCrittersRu
{
base.Started(ent, ref args);
if (!Station.TryGetRandomStation<StationEventEligibleComponent>(out var station))
{
return;
}
var critterRule = ent.Comp1;
var locations = EntityQueryEnumerator<VentCritterSpawnLocationComponent, TransformComponent>();
var validLocations = new List<EntityCoordinates>();
while (locations.MoveNext(out _, out _, out var transform))
{
if (!transform.Anchored)
continue;
if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station == station.Value.Owner)
{
validLocations.Add(transform.Coordinates);
foreach (var spawn in EntitySpawnCollection.GetSpawns(critterRule.Entries, RobustRandom))
{
Spawn(spawn, transform.Coordinates);
}
}
}
if (critterRule.SpecialEntries.Count == 0 || validLocations.Count == 0)
{
var validLocations = Station.GetEntitiesWithComponentOnStation<VentCritterSpawnLocationComponent>(true);
if (validLocations.Count == 0)
return;
}
// guaranteed spawn
var specialEntry = RobustRandom.Pick(critterRule.SpecialEntries);
var specialSpawn = RobustRandom.Pick(validLocations);
Spawn(specialEntry.PrototypeId, specialSpawn);
if (critterRule.SpecialEntries.Count > 0)
{
var specialEntry = RobustRandom.Pick(critterRule.SpecialEntries);
var specialSpawn = Transform(RobustRandom.Pick(validLocations)).Coordinates;
Spawn(specialEntry.PrototypeId, specialSpawn);
}
foreach (var location in validLocations)
{
foreach (var spawn in EntitySpawnCollection.GetSpawns(critterRule.SpecialEntries, RobustRandom))
var spawns = EntitySpawnCollection.GetSpawns(critterRule.Entries, RobustRandom);
foreach (var spawn in spawns)
{
Spawn(spawn, location);
Spawn(spawn, Transform(location).Coordinates);
}
var specialSpawns = EntitySpawnCollection.GetSpawns(critterRule.SpecialEntries, RobustRandom);
foreach (var spawn in specialSpawns)
{
Spawn(spawn, Transform(location).Coordinates);
}
}
}

View file

@ -5,7 +5,6 @@ using Content.Server.VentHorde.Components;
using Content.Server.VentHorde.Systems;
using Content.Shared.EntityTable;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@ -26,8 +25,6 @@ public sealed partial class VentHordeRule : StationEventSystem<VentHordeRuleComp
[Dependency] private VentHordeSystem _horde = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private EntityQuery<VentHordeSpawnerComponent> _hordeSpawnerQuery;
protected override void Added(Entity<VentHordeRuleComponent, GameRuleComponent> ent, ref GameRuleAddedEvent args)
{
var ventHorde = ent.Comp1;
@ -76,7 +73,7 @@ public sealed partial class VentHordeRule : StationEventSystem<VentHordeRuleComp
var ventHorde = ent.Comp1;
// We grab when the gamerule is expected to end and subtract the current time from it to get the duration.
var duration = (stationEventComp.EndTime - _timing.CurTime) ?? TimeSpan.Zero;
var duration = stationEventComp.EndTime - _timing.CurTime ?? TimeSpan.Zero;
var spawns = _table.GetSpawns(ventHorde.Table);
@ -91,30 +88,8 @@ public sealed partial class VentHordeRule : StationEventSystem<VentHordeRuleComp
private EntityUid? ChooseVent()
{
// Get a station
if (!Station.TryGetRandomStation<StationEventEligibleComponent>(out var station))
{
return null;
}
// Query the possible locations
var locations = EntityQueryEnumerator<VentCritterSpawnLocationComponent, TransformComponent>();
var validLocations = new List<EntityUid>();
// Filter to things on the same station
while (locations.MoveNext(out var uid, out _, out var transform))
{
if (!transform.Anchored)
continue;
if (_hordeSpawnerQuery.HasComp(uid))
continue;
if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station == station.Value.Owner)
{
validLocations.Add(uid);
}
}
var validLocations = Station.GetEntitiesWithComponentOnStation<VentCritterSpawnLocationComponent>(true);
validLocations.RemoveWhere(uid => HasComp<VentHordeSpawnerComponent>(uid));
// Pick one at random
if (validLocations.Count != 0)

View file

@ -158,9 +158,6 @@
duration: 1
minimumPlayers: 15
- type: BreakerFlipRule
blacklist:
components:
- ProtectedGrid
- type: DynamicRuleCost
cost: 10