]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Roundstart variation game rules (#24397)
authorKara <lunarautomaton6@gmail.com>
Wed, 31 Jan 2024 05:52:35 +0000 (22:52 -0700)
committerGitHub <noreply@github.com>
Wed, 31 Jan 2024 05:52:35 +0000 (21:52 -0800)
* Raise `StationPostInitEvent` broadcast

* Basic variation pass handling

* standardize names + rule entities

* why does it work like that?

* add to defaults

* light break variation pass

* ent spawn entry

* move some stationevent utility functions to gamerule + add one for finding random tile on specified station

* forgot how statistics works

* powered light variation pass is good now

* station tile count function

* public method to ensure all solutions (for procedural use before mapinit)

* move gamerulesystem utility funcs to partial

* ensure all solutions before spilling in puddlesystem. for use when spilling before mapinit

* trash & puddle variation passes!

* oh yeah

* ehh lets live a little

* std

* utility for game rule check based on comp

* entprotoid the trash spawner oops

* generalize trash variation

* use added instead of started for secret rule

* random cleanup

* generic replacement variation system

* Wall rusting variation rule

* account for modifying while enumerating

* use localaabb

* fix test

* minor tweaks

* reinforced wall replacer + puddletweaker

37 files changed:
Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
Content.Server/Chemistry/Containers/EntitySystems/SolutionContainerSystem.cs
Content.Server/Fluids/EntitySystems/PuddleSystem.cs
Content.Server/GameTicking/GameTicker.GamePreset.cs
Content.Server/GameTicking/GameTicker.GameRule.cs
Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/GameRuleSystem.cs
Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/SecretRuleSystem.cs
Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/EntityReplaceVariationPassComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/EntitySpawnVariationPassComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/PoweredLightVariationPassComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/PuddleMessVariationPassComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/ReinforcedWallReplaceVariationPassComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/ReinforcedWallReplacementMarkerComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/WallReplacementMarkerComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/Components/WallReplaceVariationPassComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/ReinforcedWallReplaceVariationPassSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/VariationPass/WallReplaceVariationPassSystem.cs [new file with mode: 0644]
Content.Server/Light/EntitySystems/PoweredLightSystem.cs
Content.Server/Station/Components/StationVariationHasRunComponent.cs [new file with mode: 0644]
Content.Server/Station/Events/StationPostInitEvent.cs
Content.Server/Station/Systems/StationSystem.cs
Content.Server/StationEvents/Events/StationEventSystem.cs
Content.Shared/Storage/EntitySpawnEntry.cs
Resources/Prototypes/Entities/Objects/Power/lights.yml
Resources/Prototypes/Entities/Structures/Walls/walls.yml
Resources/Prototypes/GameRules/roundstart.yml
Resources/Prototypes/GameRules/variation.yml [new file with mode: 0644]
Resources/Prototypes/game_presets.yml

index 941337f7ed30dec50743a7408d799b2d494731de..0f665a63de03c3c05b2a3b39139de78648b86ba1 100644 (file)
@@ -21,7 +21,10 @@ public sealed class SecretStartsTest
 
         await server.WaitAssertion(() =>
         {
-            gameTicker.StartGameRule("Secret");
+            // this mimics roundflow:
+            // rules added, then round starts
+            gameTicker.AddGameRule("Secret");
+            gameTicker.StartGamePresetRules();
         });
 
         // Wait three ticks for any random update loops that might happen
index fcf68013fa537ac186c18d73e52fac3462dfd8ba..7926121c2b3345bd8a5c8b9dbfcf5f197cfaa0bb 100644 (file)
@@ -30,10 +30,10 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
     public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, out bool existed)
         => EnsureSolution(entity, name, FixedPoint2.Zero, out existed);
 
-    public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 minVol, out bool existed)
-        => EnsureSolution(entity, name, minVol, null, out existed);
+    public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, out bool existed)
+        => EnsureSolution(entity, name, maxVol, null, out existed);
 
-    public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed)
+    public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
     {
         var (uid, meta) = entity;
         if (!Resolve(uid, ref meta))
@@ -41,12 +41,26 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
 
         var manager = EnsureComp<SolutionContainerManagerComponent>(uid);
         if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized)
-            return EnsureSolutionEntity((uid, manager), name, minVol, prototype, out existed).Comp.Solution;
+            return EnsureSolutionEntity((uid, manager), name, maxVol, prototype, out existed).Comp.Solution;
         else
-            return EnsureSolutionPrototype((uid, manager), name, minVol, prototype, out existed);
+            return EnsureSolutionPrototype((uid, manager), name, maxVol, prototype, out existed);
     }
 
-    public Entity<SolutionComponent> EnsureSolutionEntity(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed)
+    public void EnsureAllSolutions(Entity<SolutionContainerManagerComponent> entity)
+    {
+        if (entity.Comp.Solutions is not { } prototypes)
+            return;
+
+        foreach (var (name, prototype) in prototypes)
+        {
+            EnsureSolutionEntity((entity.Owner, entity.Comp), name, prototype.MaxVolume, prototype, out _);
+        }
+
+        entity.Comp.Solutions = null;
+        Dirty(entity);
+    }
+
+    public Entity<SolutionComponent> EnsureSolutionEntity(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
     {
         existed = true;
 
@@ -69,9 +83,9 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
         SolutionComponent solutionComp;
         if (solutionSlot.ContainedEntity is not { } solutionId)
         {
-            prototype ??= new() { MaxVolume = minVol };
+            prototype ??= new() { MaxVolume = maxVol };
             prototype.Name = name;
-            (solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, minVol, prototype);
+            (solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, maxVol, prototype);
             existed = false;
             needsInit = true;
             Dirty(uid, container);
@@ -83,7 +97,7 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
             DebugTools.Assert(solutionComp.Solution.Name == name);
 
             var solution = solutionComp.Solution;
-            solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, minVol);
+            solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
 
             // Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions.
             // We want the reagents from the prototype to exist even if something else already created the solution.
@@ -99,7 +113,7 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
         return (solutionId, solutionComp);
     }
 
-    private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed)
+    private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
     {
         existed = true;
 
@@ -115,19 +129,19 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
 
         if (!container.Solutions.TryGetValue(name, out var solution))
         {
-            solution = prototype ?? new() { Name = name, MaxVolume = minVol };
+            solution = prototype ?? new() { Name = name, MaxVolume = maxVol };
             container.Solutions.Add(name, solution);
             existed = false;
         }
         else
-            solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, minVol);
+            solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
 
         Dirty(uid, container);
         return solution;
     }
 
 
-    private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 minVol, Solution prototype)
+    private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
     {
         var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
         var uid = EntityManager.CreateEntityUninitialized(null, coords, null);
@@ -148,16 +162,7 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
 
     private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
     {
-        if (entity.Comp.Solutions is not { } prototypes)
-            return;
-
-        foreach (var (name, prototype) in prototypes)
-        {
-            EnsureSolutionEntity((entity.Owner, entity.Comp), name, prototype.MaxVolume, prototype, out _);
-        }
-
-        entity.Comp.Solutions = null;
-        Dirty(entity);
+        EnsureAllSolutions(entity);
     }
 
     private void OnComponentShutdown(Entity<SolutionContainerManagerComponent> entity, ref ComponentShutdown args)
index c5eb42e05b55d5a13fab09ecf7c2e8dd39373311..b6df3a171b849cff42ad626689ddff2e2a6e9100 100644 (file)
@@ -5,6 +5,7 @@ using Content.Server.Fluids.Components;
 using Content.Server.Spreader;
 using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Chemistry.Reaction;
 using Content.Shared.Chemistry.Reagent;
@@ -505,11 +506,14 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         Solution addedSolution,
         bool sound = true,
         bool checkForOverflow = true,
-        PuddleComponent? puddleComponent = null)
+        PuddleComponent? puddleComponent = null,
+        SolutionContainerManagerComponent? sol = null)
     {
-        if (!Resolve(puddleUid, ref puddleComponent))
+        if (!Resolve(puddleUid, ref puddleComponent, ref sol))
             return false;
 
+        _solutionContainerSystem.EnsureAllSolutions((puddleUid, sol));
+
         if (addedSolution.Volume == 0 ||
             !_solutionContainerSystem.ResolveSolution(puddleUid, puddleComponent.SolutionName,
                 ref puddleComponent.Solution))
index 04f7be016a0346b6eb02140f594024124caa25bf..b97a16ab99328d4d27c63b6c3d621dc7acf89e82 100644 (file)
@@ -188,7 +188,7 @@ namespace Content.Server.GameTicking
             return true;
         }
 
-        private void StartGamePresetRules()
+        public void StartGamePresetRules()
         {
             // May be touched by the preset during init.
             var rules = new List<EntityUid>(GetAddedGameRules());
index 971e103c1b577a47cfe8c74b6a3ecf410f1973c0..4ebe946af4ae1aa97e0ddf00e34d063409ed8925 100644 (file)
@@ -141,6 +141,24 @@ public sealed partial class GameTicker
         return true;
     }
 
+    /// <summary>
+    ///     Returns true if a game rule with the given component has been added.
+    /// </summary>
+    public bool IsGameRuleAdded<T>()
+        where T : IComponent
+    {
+        var query = EntityQueryEnumerator<T, GameRuleComponent>();
+        while (query.MoveNext(out var uid, out _, out _))
+        {
+            if (HasComp<EndedGameRuleComponent>(uid))
+                continue;
+
+            return true;
+        }
+
+        return false;
+    }
+
     public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null)
     {
         return Resolve(ruleEntity, ref component) && !HasComp<EndedGameRuleComponent>(ruleEntity);
@@ -157,6 +175,22 @@ public sealed partial class GameTicker
         return false;
     }
 
+    /// <summary>
+    ///     Returns true if a game rule with the given component is active..
+    /// </summary>
+    public bool IsGameRuleActive<T>()
+        where T : IComponent
+    {
+        var query = EntityQueryEnumerator<T, ActiveGameRuleComponent, GameRuleComponent>();
+        // out, damned underscore!!!
+        while (query.MoveNext(out _, out _, out _, out _))
+        {
+            return true;
+        }
+
+        return false;
+    }
+
     public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null)
     {
         return Resolve(ruleEntity, ref component) && HasComp<ActiveGameRuleComponent>(ruleEntity);
diff --git a/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs
new file mode 100644 (file)
index 0000000..44ae89f
--- /dev/null
@@ -0,0 +1,19 @@
+using Content.Shared.Storage;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+/// This handles starting various roundstart variation rules after a station has been loaded.
+/// </summary>
+[RegisterComponent]
+public sealed partial class RoundstartStationVariationRuleComponent : Component
+{
+    /// <summary>
+    ///     The list of rules that will be started once the map is spawned.
+    ///     Uses <see cref="EntitySpawnEntry"/> to support probabilities for various rules
+    ///     without having to hardcode the probability directly in the rule's logic.
+    /// </summary>
+    [DataField(required: true)]
+    public List<EntitySpawnEntry> Rules = new();
+}
diff --git a/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs
new file mode 100644 (file)
index 0000000..9fdc62a
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+/// This is a marker component placed on rule entities which are a single "pass" of station variation.
+/// </summary>
+[RegisterComponent]
+public sealed partial class StationVariationPassRuleComponent : Component
+{
+}
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
new file mode 100644 (file)
index 0000000..9d75e65
--- /dev/null
@@ -0,0 +1,137 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Station.Components;
+using Robust.Shared.Collections;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules;
+
+public abstract partial class GameRuleSystem<T> where T: IComponent
+{
+    protected EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent> QueryActiveRules()
+    {
+        return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
+    }
+
+    protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
+    {
+        var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
+        while (query.MoveNext(out _, out _, out _, out var gameRule))
+        {
+            var minPlayers = gameRule.MinPlayers;
+            if (!ev.Forced && ev.Players.Length < minPlayers)
+            {
+                ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
+                    ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
+                    ("presetName", localizedPresetName)));
+                ev.Cancel();
+                continue;
+            }
+
+            if (ev.Players.Length == 0)
+            {
+                ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
+                ev.Cancel();
+            }
+        }
+
+        return !ev.Cancelled;
+    }
+
+    /// <summary>
+    ///     Utility function for finding a random event-eligible station entity
+    /// </summary>
+    protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func<EntityUid, bool>? filter = null)
+    {
+        var stations = new ValueList<EntityUid>(Count<StationEventEligibleComponent>());
+
+        filter ??= _ => true;
+        var query = AllEntityQuery<StationEventEligibleComponent>();
+
+        while (query.MoveNext(out var uid, out _))
+        {
+            if (!filter(uid))
+                continue;
+
+            stations.Add(uid);
+        }
+
+        if (stations.Count == 0)
+        {
+            station = null;
+            return false;
+        }
+
+        // TODO: Engine PR.
+        station = stations[RobustRandom.Next(stations.Count)];
+        return true;
+    }
+
+    protected bool TryFindRandomTile(out Vector2i tile,
+        [NotNullWhen(true)] out EntityUid? targetStation,
+        out EntityUid targetGrid,
+        out EntityCoordinates targetCoords)
+    {
+        tile = default;
+        targetStation = EntityUid.Invalid;
+        targetGrid = EntityUid.Invalid;
+        targetCoords = EntityCoordinates.Invalid;
+        if (TryGetRandomStation(out targetStation))
+        {
+            return TryFindRandomTileOnStation((targetStation.Value, Comp<StationDataComponent>(targetStation.Value)),
+                out tile,
+                out targetGrid,
+                out targetCoords);
+        }
+
+        return false;
+    }
+
+    protected bool TryFindRandomTileOnStation(Entity<StationDataComponent> station,
+        out Vector2i tile,
+        out EntityUid targetGrid,
+        out EntityCoordinates targetCoords)
+    {
+        tile = default;
+        targetCoords = EntityCoordinates.Invalid;
+        targetGrid = EntityUid.Invalid;
+
+        var possibleTargets = station.Comp.Grids;
+        if (possibleTargets.Count == 0)
+        {
+            targetGrid = EntityUid.Invalid;
+            return false;
+        }
+
+        targetGrid = RobustRandom.Pick(possibleTargets);
+
+        if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
+            return false;
+
+        var found = false;
+        var aabb = gridComp.LocalAABB;
+
+        for (var i = 0; i < 10; i++)
+        {
+            var randomX = RobustRandom.Next((int) aabb.Left, (int) aabb.Right);
+            var randomY = RobustRandom.Next((int) aabb.Bottom, (int) aabb.Top);
+
+            tile = new Vector2i(randomX, randomY);
+            if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile,
+                    mapGridComp: gridComp)
+                || _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp))
+            {
+                continue;
+            }
+
+            found = true;
+            targetCoords = _map.GridTileToLocal(targetGrid, gridComp, tile);
+            break;
+        }
+
+        return found;
+    }
+
+}
index ba781e32e298dc12a097f52e778e83bd72825fa4..1667e73d046438f9fe04ffc2456400cd40a76d7d 100644 (file)
@@ -1,13 +1,27 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Server.Atmos.EntitySystems;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Station.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Collections;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
 
 namespace Content.Server.GameTicking.Rules;
 
 public abstract partial class GameRuleSystem<T> : EntitySystem where T : IComponent
 {
+    [Dependency] protected readonly IRobustRandom RobustRandom = default!;
     [Dependency] protected readonly IChatManager ChatManager = default!;
     [Dependency] protected readonly GameTicker GameTicker = default!;
 
+    // Not protected, just to be used in utility methods
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+    [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+    [Dependency] private readonly MapSystem _map = default!;
+
     public override void Initialize()
     {
         base.Initialize();
@@ -71,36 +85,6 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
 
     }
 
-    protected EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent> QueryActiveRules()
-    {
-        return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
-    }
-
-    protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
-    {
-        var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
-        while (query.MoveNext(out _, out _, out _, out var gameRule))
-        {
-            var minPlayers = gameRule.MinPlayers;
-            if (!ev.Forced && ev.Players.Length < minPlayers)
-            {
-                ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
-                    ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
-                    ("presetName", localizedPresetName)));
-                ev.Cancel();
-                continue;
-            }
-
-            if (ev.Players.Length == 0)
-            {
-                ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
-                ev.Cancel();
-            }
-        }
-
-        return !ev.Cancelled;
-    }
-
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
diff --git a/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs b/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs
new file mode 100644 (file)
index 0000000..7755f68
--- /dev/null
@@ -0,0 +1,70 @@
+using System.Linq;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Shuttles.Systems;
+using Content.Server.Station.Components;
+using Content.Server.Station.Events;
+using Content.Shared.Storage;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules;
+
+/// <inheritdoc cref="RoundstartStationVariationRuleComponent"/>
+public sealed class RoundstartStationVariationRuleSystem : GameRuleSystem<RoundstartStationVariationRuleComponent>
+{
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit, after: new []{typeof(ShuttleSystem)});
+    }
+
+    protected override void Added(EntityUid uid, RoundstartStationVariationRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+    {
+        var spawns = EntitySpawnCollection.GetSpawns(component.Rules, _random);
+        foreach (var rule in spawns)
+        {
+            GameTicker.AddGameRule(rule);
+        }
+    }
+
+    private void OnStationPostInit(ref StationPostInitEvent ev)
+    {
+        // as long as one is running
+        if (!GameTicker.IsGameRuleAdded<RoundstartStationVariationRuleComponent>())
+            return;
+
+        // this is unlikely, but could theoretically happen if it was saved and reloaded, so check anyway
+        if (HasComp<StationVariationHasRunComponent>(ev.Station))
+            return;
+
+        Log.Info($"Running variation rules for station {ToPrettyString(ev.Station)}");
+
+        // raise the event on any passes that have been added
+        var passEv = new StationVariationPassEvent(ev.Station);
+        var passQuery = EntityQueryEnumerator<StationVariationPassRuleComponent, GameRuleComponent>();
+        while (passQuery.MoveNext(out var uid, out _, out _))
+        {
+            // TODO: for some reason, ending a game rule just gives it a marker comp,
+            // and doesnt delete it
+            // so we have to check here that it isnt an ended game rule (which could happen if a preset failed to start
+            // or it was ended before station maps spawned etc etc etc)
+            if (HasComp<EndedGameRuleComponent>(uid))
+                continue;
+
+            RaiseLocalEvent(uid, ref passEv);
+        }
+
+        EnsureComp<StationVariationHasRunComponent>(ev.Station);
+    }
+}
+
+/// <summary>
+///     Raised directed on game rule entities which are added and marked as <see cref="StationVariationPassRuleComponent"/>
+///     when a new station is initialized that should be varied.
+/// </summary>
+/// <param name="Station">The new station that was added, and its config & grids.</param>
+[ByRefEvent]
+public readonly record struct StationVariationPassEvent(Entity<StationDataComponent> Station);
index 1e3858ceef6f80582a862c11324b189b926b1aca..6a00eb7d102e50f952d4a2658abdf37b05028a29 100644 (file)
@@ -18,9 +18,9 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
     [Dependency] private readonly IConfigurationManager _configurationManager = default!;
     [Dependency] private readonly IAdminLogManager _adminLogger = default!;
 
-    protected override void Started(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+    protected override void Added(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
     {
-        base.Started(uid, component, gameRule, args);
+        base.Added(uid, component, gameRule, args);
         PickRule(component);
     }
 
@@ -40,13 +40,24 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
         // but currently there's no way to know anyway as they use cvars.
         var presetString = _configurationManager.GetCVar(CCVars.SecretWeightPrototype);
         var preset = _prototypeManager.Index<WeightedRandomPrototype>(presetString).Pick(_random);
-        Logger.InfoS("gamepreset", $"Selected {preset} for secret.");
+        Log.Info($"Selected {preset} for secret.");
         _adminLogger.Add(LogType.EventStarted, $"Selected {preset} for secret.");
 
         var rules = _prototypeManager.Index<GamePresetPrototype>(preset).Rules;
         foreach (var rule in rules)
         {
-            GameTicker.StartGameRule(rule, out var ruleEnt);
+            EntityUid ruleEnt;
+
+            // if we're pre-round (i.e. will only be added)
+            // then just add rules. if we're added in the middle of the round (or at any other point really)
+            // then we want to start them as well
+            if (GameTicker.RunLevel <= GameRunLevel.InRound)
+                ruleEnt = GameTicker.AddGameRule(rule);
+            else
+            {
+                GameTicker.StartGameRule(rule, out ruleEnt);
+            }
+
             component.AdditionalGameRules.Add(ruleEnt);
         }
     }
diff --git a/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs
new file mode 100644 (file)
index 0000000..00b2546
--- /dev/null
@@ -0,0 +1,75 @@
+using Content.Server.GameTicking.Rules.VariationPass.Components;
+using Content.Shared.Storage;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <inheritdoc cref="EntityReplaceVariationPassComponent"/>
+/// <summary>
+///     A base system for fast replacement of entities utilizing a query, rather than having to iterate every entity
+///     To use, you must have a marker component to use for <see cref="TEntComp"/>--each replaceable entity must have it
+///     Then you need an inheriting system as well as a unique game rule component for <see cref="TGameRuleComp"/>
+///
+///     This means a bit more boilerplate for each one, but significantly faster to actually execute.
+///     See <see cref="WallReplaceVariationPassSystem"/>
+/// </summary>
+public abstract class BaseEntityReplaceVariationPassSystem<TEntComp, TGameRuleComp> : VariationPassSystem<TGameRuleComp>
+    where TEntComp: IComponent
+    where TGameRuleComp: IComponent
+{
+    /// <summary>
+    ///     Used so we don't modify while enumerating
+    ///     if the replaced entity also has <see cref="TEntComp"/>.
+    ///
+    ///     Filled and cleared within the same tick so no persistence issues.
+    /// </summary>
+    private readonly Queue<(string, EntityCoordinates, Angle)> _queuedSpawns = new();
+
+    protected override void ApplyVariation(Entity<TGameRuleComp> ent, ref StationVariationPassEvent args)
+    {
+        if (!TryComp<EntityReplaceVariationPassComponent>(ent, out var pass))
+            return;
+
+        var stopwatch = new Stopwatch();
+        stopwatch.Start();
+
+        var replacementMod = Random.NextGaussian(pass.EntitiesPerReplacementAverage, pass.EntitiesPerReplacementStdDev);
+        var prob = (float) Math.Clamp(1 / replacementMod, 0f, 1f);
+
+        if (prob == 0)
+            return;
+
+        var enumerator = AllEntityQuery<TEntComp, TransformComponent>();
+        while (enumerator.MoveNext(out var uid, out _, out var xform))
+        {
+            if (!IsMemberOfStation((uid, xform), ref args))
+                continue;
+
+            if (RobustRandom.Prob(prob))
+                QueueReplace((uid, xform), pass.Replacements);
+        }
+
+        while (_queuedSpawns.TryDequeue(out var tup))
+        {
+            var (spawn, coords, rot) = tup;
+            var newEnt = Spawn(spawn, coords);
+            Transform(newEnt).LocalRotation = rot;
+        }
+
+        Log.Debug($"Entity replacement took {stopwatch.Elapsed} with {Stations.GetTileCount(args.Station)} tiles");
+    }
+
+    private void QueueReplace(Entity<TransformComponent> ent, List<EntitySpawnEntry> replacements)
+    {
+        var coords = ent.Comp.Coordinates;
+        var rot = ent.Comp.LocalRotation;
+        QueueDel(ent);
+
+        foreach (var spawn in EntitySpawnCollection.GetSpawns(replacements, RobustRandom))
+        {
+            _queuedSpawns.Enqueue((spawn, coords, rot));
+        }
+    }
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/EntityReplaceVariationPassComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/EntityReplaceVariationPassComponent.cs
new file mode 100644 (file)
index 0000000..6603251
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Shared.Storage;
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+/// <summary>
+/// This is used for replacing a certain amount of entities with other entities in a variation pass.
+///
+/// </summary>
+/// <remarks>
+/// POTENTIALLY REPLACEABLE ENTITIES MUST BE MARKED WITH A REPLACEMENT MARKER
+/// AND HAVE A SYSTEM INHERITING FROM <see cref="BaseEntityReplaceVariationPassSystem{TEntComp,TGameRuleComp}"/>
+/// SEE <see cref="WallReplaceVariationPassSystem"/>
+/// </remarks>
+[RegisterComponent]
+public sealed partial class EntityReplaceVariationPassComponent : Component
+{
+    /// <summary>
+    ///     Number of matching entities before one will be replaced on average.
+    /// </summary>
+    [DataField(required: true)]
+    public float EntitiesPerReplacementAverage;
+
+    [DataField(required: true)]
+    public float EntitiesPerReplacementStdDev;
+
+    /// <summary>
+    ///     Prototype(s) to replace matched entities with.
+    /// </summary>
+    [DataField(required: true)]
+    public List<EntitySpawnEntry> Replacements = default!;
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/EntitySpawnVariationPassComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/EntitySpawnVariationPassComponent.cs
new file mode 100644 (file)
index 0000000..f7ddd7c
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Random;
+using Content.Shared.Storage;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+/// <summary>
+/// This is used for spawning entities randomly dotted around the station in a variation pass.
+/// </summary>
+[RegisterComponent]
+public sealed partial class EntitySpawnVariationPassComponent : Component
+{
+    /// <summary>
+    ///     Number of tiles before we spawn one entity on average.
+    /// </summary>
+    [DataField]
+    public float TilesPerEntityAverage = 50f;
+
+    [DataField]
+    public float TilesPerEntityStdDev = 7f;
+
+    /// <summary>
+    ///     Spawn entries for each chosen location.
+    /// </summary>
+    [DataField(required: true)]
+    public List<EntitySpawnEntry> Entities = default!;
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/PoweredLightVariationPassComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/PoweredLightVariationPassComponent.cs
new file mode 100644 (file)
index 0000000..98c58e0
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Shared.Light.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+/// <summary>
+/// This handle randomly destroying lights, causing them to flicker endlessly, or replacing their tube/bulb with different variants.
+/// </summary>
+[RegisterComponent]
+public sealed partial class PoweredLightVariationPassComponent : Component
+{
+    /// <summary>
+    ///     Chance that a light will be replaced with a broken variant.
+    /// </summary>
+    [DataField]
+    public float LightBreakChance = 0.15f;
+
+    /// <summary>
+    ///     Chance that a light will be replaced with an aged variant.
+    /// </summary>
+    [DataField]
+    public float LightAgingChance = 0.05f;
+
+    [DataField]
+    public float AgedLightTubeFlickerChance = 0.03f;
+
+    [DataField]
+    public EntProtoId BrokenLightBulbPrototype = "LightBulbBroken";
+
+    [DataField]
+    public EntProtoId BrokenLightTubePrototype = "LightTubeBroken";
+
+    [DataField]
+    public EntProtoId AgedLightBulbPrototype = "LightBulbOld";
+
+    [DataField]
+    public EntProtoId AgedLightTubePrototype = "LightTubeOld";
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/PuddleMessVariationPassComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/PuddleMessVariationPassComponent.cs
new file mode 100644 (file)
index 0000000..787d338
--- /dev/null
@@ -0,0 +1,26 @@
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+/// <summary>
+///     Handles spilling puddles with various reagents randomly around the station.
+/// </summary>
+[RegisterComponent]
+public sealed partial class PuddleMessVariationPassComponent : Component
+{
+    /// <summary>
+    ///     Tiles before one spill on average.
+    /// </summary>
+    [DataField]
+    public float TilesPerSpillAverage = 600f;
+
+    [DataField]
+    public float TilesPerSpillStdDev = 50f;
+
+    /// <summary>
+    ///     Weighted random prototype to use for random messes.
+    /// </summary>
+    [DataField(required: true)]
+    public ProtoId<WeightedRandomFillSolutionPrototype> RandomPuddleSolutionFill = default!;
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/ReinforcedWallReplaceVariationPassComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/ReinforcedWallReplaceVariationPassComponent.cs
new file mode 100644 (file)
index 0000000..82095fe
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+
+[RegisterComponent]
+public sealed partial class ReinforcedWallReplaceVariationPassComponent : Component
+{
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/ReinforcedWallReplacementMarkerComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/ReinforcedWallReplacementMarkerComponent.cs
new file mode 100644 (file)
index 0000000..00f55b7
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
+
+/// <summary>
+/// This component marks replaceable reinforced walls for use with fast queries in variation passes.
+/// </summary>
+[RegisterComponent]
+public sealed partial class ReinforcedWallReplacementMarkerComponent : Component
+{
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/WallReplacementMarkerComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/WallReplacementMarkerComponent.cs
new file mode 100644 (file)
index 0000000..6df0432
--- /dev/null
@@ -0,0 +1,9 @@
+namespace Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
+
+/// <summary>
+/// This component marks replaceable walls for use with fast queries in variation passes.
+/// </summary>
+[RegisterComponent]
+public sealed partial class WallReplacementMarkerComponent : Component
+{
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/Components/WallReplaceVariationPassComponent.cs b/Content.Server/GameTicking/Rules/VariationPass/Components/WallReplaceVariationPassComponent.cs
new file mode 100644 (file)
index 0000000..3598dee
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+
+[RegisterComponent]
+public sealed partial class WallReplaceVariationPassComponent : Component
+{
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs
new file mode 100644 (file)
index 0000000..7247bd9
--- /dev/null
@@ -0,0 +1,29 @@
+using Content.Server.GameTicking.Rules.VariationPass.Components;
+using Content.Shared.Storage;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <inheritdoc cref="EntitySpawnVariationPassComponent"/>
+public sealed class EntitySpawnVariationPassSystem : VariationPassSystem<EntitySpawnVariationPassComponent>
+{
+    protected override void ApplyVariation(Entity<EntitySpawnVariationPassComponent> ent, ref StationVariationPassEvent args)
+    {
+        var totalTiles = Stations.GetTileCount(args.Station);
+
+        var dirtyMod = Random.NextGaussian(ent.Comp.TilesPerEntityAverage, ent.Comp.TilesPerEntityStdDev);
+        var trashTiles = Math.Max((int) (totalTiles * (1 / dirtyMod)), 0);
+
+        for (var i = 0; i < trashTiles; i++)
+        {
+            if (!TryFindRandomTileOnStation(args.Station, out _, out _, out var coords))
+                continue;
+
+            var ents = EntitySpawnCollection.GetSpawns(ent.Comp.Entities, Random);
+            foreach (var spawn in ents)
+            {
+                SpawnAtPosition(spawn, coords);
+            }
+        }
+    }
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs
new file mode 100644 (file)
index 0000000..dae6981
--- /dev/null
@@ -0,0 +1,51 @@
+using Content.Server.GameTicking.Rules.VariationPass.Components;
+using Content.Server.Light.Components;
+using Content.Server.Light.EntitySystems;
+using Content.Shared.Light.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <inheritdoc cref="PoweredLightVariationPassComponent"/>
+public sealed class PoweredLightVariationPassSystem : VariationPassSystem<PoweredLightVariationPassComponent>
+{
+    [Dependency] private readonly PoweredLightSystem _poweredLight = default!;
+
+    protected override void ApplyVariation(Entity<PoweredLightVariationPassComponent> ent, ref StationVariationPassEvent args)
+    {
+        var query = AllEntityQuery<PoweredLightComponent, TransformComponent>();
+        while (query.MoveNext(out var uid, out var comp, out var xform))
+        {
+            if (!IsMemberOfStation((uid, xform), ref args))
+                continue;
+
+            if (Random.Prob(ent.Comp.LightBreakChance))
+            {
+                var proto = comp.BulbType switch
+                {
+                    LightBulbType.Tube => ent.Comp.BrokenLightTubePrototype,
+                    _ => ent.Comp.BrokenLightBulbPrototype,
+                };
+
+                _poweredLight.ReplaceSpawnedPrototype((uid, comp), proto);
+                continue;
+            }
+
+            if (!Random.Prob(ent.Comp.LightAgingChance))
+                continue;
+
+            if (comp.BulbType == LightBulbType.Tube)
+            {
+                // some aging fluorescents (tubes) start to flicker
+                // its also way too annoying right now so we wrap it in another prob lol
+                if (Random.Prob(ent.Comp.AgedLightTubeFlickerChance))
+                    _poweredLight.ToggleBlinkingLight(uid, comp, true);
+                _poweredLight.ReplaceSpawnedPrototype((uid, comp), ent.Comp.AgedLightTubePrototype);
+            }
+            else
+            {
+                _poweredLight.ReplaceSpawnedPrototype((uid, comp), ent.Comp.AgedLightBulbPrototype);
+            }
+        }
+    }
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs
new file mode 100644 (file)
index 0000000..41cdbd8
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.GameTicking.Rules.VariationPass.Components;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <inheritdoc cref="PuddleMessVariationPassComponent"/>
+public sealed class PuddleMessVariationPassSystem : VariationPassSystem<PuddleMessVariationPassComponent>
+{
+    [Dependency] private readonly PuddleSystem _puddle = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+
+    protected override void ApplyVariation(Entity<PuddleMessVariationPassComponent> ent, ref StationVariationPassEvent args)
+    {
+        var totalTiles = Stations.GetTileCount(args.Station);
+
+        if (!_proto.TryIndex(ent.Comp.RandomPuddleSolutionFill, out var proto))
+            return;
+
+        var puddleMod = Random.NextGaussian(ent.Comp.TilesPerSpillAverage, ent.Comp.TilesPerSpillStdDev);
+        var puddleTiles = Math.Max((int) (totalTiles * (1 / puddleMod)), 0);
+
+        for (var i = 0; i < puddleTiles; i++)
+        {
+            if (!TryFindRandomTileOnStation(args.Station, out _, out _, out var coords))
+                continue;
+
+            var sol = proto.Pick(Random);
+            _puddle.TrySpillAt(coords, new Solution(sol.reagent, sol.quantity), out _, sound: false);
+        }
+    }
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/ReinforcedWallReplaceVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/ReinforcedWallReplaceVariationPassSystem.cs
new file mode 100644 (file)
index 0000000..1950581
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.GameTicking.Rules.VariationPass.Components;
+using Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <summary>
+/// This handles the ability to replace entities marked with <see cref="ReinforcedWallReplacementMarkerComponent"/> in a variation pass
+/// </summary>
+public sealed class ReinforcedWallReplaceVariationPassSystem : BaseEntityReplaceVariationPassSystem<ReinforcedWallReplacementMarkerComponent, ReinforcedWallReplaceVariationPassComponent>
+{
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs
new file mode 100644 (file)
index 0000000..b6ead21
--- /dev/null
@@ -0,0 +1,29 @@
+using Content.Server.Station.Systems;
+using Robust.Shared.Random;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <summary>
+///     Base class for procedural variation rule passes, which apply some kind of variation to a station,
+///     so we simply reduce the boilerplate for the event handling a bit with this.
+/// </summary>
+public abstract class VariationPassSystem<T> : GameRuleSystem<T>
+    where T: IComponent
+{
+    [Dependency] protected readonly StationSystem Stations = default!;
+    [Dependency] protected readonly IRobustRandom Random = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<T, StationVariationPassEvent>(ApplyVariation);
+    }
+
+    protected bool IsMemberOfStation(Entity<TransformComponent> ent, ref StationVariationPassEvent args)
+    {
+        return Stations.GetOwningStation(ent, ent.Comp) == args.Station.Owner;
+    }
+
+    protected abstract void ApplyVariation(Entity<T> ent, ref StationVariationPassEvent args);
+}
diff --git a/Content.Server/GameTicking/Rules/VariationPass/WallReplaceVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/WallReplaceVariationPassSystem.cs
new file mode 100644 (file)
index 0000000..e9777d8
--- /dev/null
@@ -0,0 +1,11 @@
+using Content.Server.GameTicking.Rules.VariationPass.Components;
+using Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
+
+namespace Content.Server.GameTicking.Rules.VariationPass;
+
+/// <summary>
+/// This handles the ability to replace entities marked with <see cref="WallReplacementMarkerComponent"/> in a variation pass
+/// </summary>
+public sealed class WallReplaceVariationPassSystem : BaseEntityReplaceVariationPassSystem<WallReplacementMarkerComponent, WallReplaceVariationPassComponent>
+{
+}
index e6cde4495cef82301ceecc696525ec57f299ccd8..ca44b1a4c99d8009f9a5e7e08142b65344cdb1ba 100644 (file)
@@ -214,6 +214,21 @@ namespace Content.Server.Light.EntitySystems
             return bulb;
         }
 
+        /// <summary>
+        ///     Replaces the spawned prototype of a pre-mapinit powered light with a different variant.
+        /// </summary>
+        public bool ReplaceSpawnedPrototype(Entity<PoweredLightComponent> light, string bulb)
+        {
+            if (light.Comp.LightBulbContainer.ContainedEntity != null)
+                return false;
+
+            if (LifeStage(light.Owner) >= EntityLifeStage.MapInitialized)
+                return false;
+
+            light.Comp.HasLampOnSpawn = bulb;
+            return true;
+        }
+
         /// <summary>
         ///     Try to replace current bulb with a new one
         ///     If succeed old bulb just drops on floor
@@ -241,6 +256,17 @@ namespace Content.Server.Light.EntitySystems
         /// </summary>
         public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null)
         {
+            if (!Resolve(uid, ref light, false))
+                return false;
+
+            // if we aren't mapinited,
+            // just null the spawned bulb
+            if (LifeStage(uid) < EntityLifeStage.MapInitialized)
+            {
+                light.HasLampOnSpawn = null;
+                return true;
+            }
+
             // check bulb state
             var bulbUid = GetBulb(uid, light);
             if (bulbUid == null || !EntityManager.TryGetComponent(bulbUid.Value, out LightBulbComponent? lightBulb))
diff --git a/Content.Server/Station/Components/StationVariationHasRunComponent.cs b/Content.Server/Station/Components/StationVariationHasRunComponent.cs
new file mode 100644 (file)
index 0000000..65c794f
--- /dev/null
@@ -0,0 +1,12 @@
+using Content.Server.GameTicking.Rules;
+
+namespace Content.Server.Station.Components;
+
+/// <summary>
+///     Marker component for stations where procedural variation using <see cref="RoundstartStationVariationRuleSystem"/>
+///     has already run, so as to avoid running it again if another station is added.
+/// </summary>
+[RegisterComponent]
+public sealed partial class StationVariationHasRunComponent : Component
+{
+}
index a4e55fafb275d009124835c990fecfc57231e3bc..4f7927cee525f2833d370a5d0fd6323efd87db84 100644 (file)
@@ -1,7 +1,9 @@
+using Content.Server.Station.Components;
+
 namespace Content.Server.Station.Events;
 
 /// <summary>
-/// Raised directed on a station after it has been initialized.
+/// Raised directed on a station after it has been initialized, as well as broadcast.
 /// </summary>
 [ByRefEvent]
-public readonly record struct StationPostInitEvent;
+public readonly record struct StationPostInitEvent(Entity<StationDataComponent> Station);
index a0adeb2243494a02f9fbc6083ad7bb41546a9e44..be3fc57967b3f72de8cd93366209583fae6cdfde 100644 (file)
@@ -6,6 +6,7 @@ using Content.Server.Station.Events;
 using Content.Shared.CCVar;
 using Content.Shared.Station;
 using JetBrains.Annotations;
+using Robust.Server.GameObjects;
 using Robust.Server.Player;
 using Robust.Shared.Collections;
 using Robust.Shared.Configuration;
@@ -35,6 +36,7 @@ public sealed class StationSystem : EntitySystem
     [Dependency] private readonly GameTicker _gameTicker = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly MapSystem _map = default!;
 
     private ISawmill _sawmill = default!;
 
@@ -208,6 +210,23 @@ public sealed class StationSystem : EntitySystem
         return largestGrid;
     }
 
+    /// <summary>
+    /// Returns the total number of tiles contained in the station's grids.
+    /// </summary>
+    public int GetTileCount(StationDataComponent component)
+    {
+        var count = 0;
+        foreach (var gridUid in component.Grids)
+        {
+            if (!TryComp<MapGridComponent>(gridUid, out var grid))
+                continue;
+
+            count += _map.GetAllTiles(gridUid, grid).Count();
+        }
+
+        return count;
+    }
+
     /// <summary>
     /// Tries to retrieve a filter for everything in the station the source is on.
     /// </summary>
@@ -306,8 +325,8 @@ public sealed class StationSystem : EntitySystem
             AddGridToStation(station, grid, null, data, name);
         }
 
-        var ev = new StationPostInitEvent();
-        RaiseLocalEvent(station, ref ev);
+        var ev = new StationPostInitEvent((station, data));
+        RaiseLocalEvent(station, ref ev, true);
 
         return station;
     }
index 537a7e7c2216094be2fe48aec915d02141be0492..221beccee735877ec045e9288775cc286aaaa460 100644 (file)
@@ -29,7 +29,6 @@ public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] protected readonly IMapManager MapManager = default!;
     [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
-    [Dependency] protected readonly IRobustRandom RobustRandom = default!;
     [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
     [Dependency] protected readonly ChatSystem ChatSystem = default!;
     [Dependency] protected readonly SharedAudioSystem Audio = default!;
@@ -135,79 +134,6 @@ public abstract partial class StationEventSystem<T> : GameRuleSystem<T> where T
         GameTicker.EndGameRule(uid, component);
     }
 
-    protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func<EntityUid, bool>? filter = null)
-    {
-        var stations = new ValueList<EntityUid>(Count<StationEventEligibleComponent>());
-
-        filter ??= _ => true;
-        var query = AllEntityQuery<StationEventEligibleComponent>();
-
-        while (query.MoveNext(out var uid, out _))
-        {
-            if (!filter(uid))
-                continue;
-
-            stations.Add(uid);
-        }
-
-        if (stations.Count == 0)
-        {
-            station = null;
-            return false;
-        }
-
-        // TODO: Engine PR.
-        station = stations[RobustRandom.Next(stations.Count)];
-        return true;
-    }
-
-    protected bool TryFindRandomTile(out Vector2i tile, [NotNullWhen(true)] out EntityUid? targetStation, out EntityUid targetGrid, out EntityCoordinates targetCoords)
-    {
-        tile = default;
-
-        targetCoords = EntityCoordinates.Invalid;
-        if (!TryGetRandomStation(out targetStation))
-        {
-            targetStation = EntityUid.Invalid;
-            targetGrid = EntityUid.Invalid;
-            return false;
-        }
-        var possibleTargets = Comp<StationDataComponent>(targetStation.Value).Grids;
-        if (possibleTargets.Count == 0)
-        {
-            targetGrid = EntityUid.Invalid;
-            return false;
-        }
-
-        targetGrid = RobustRandom.Pick(possibleTargets);
-
-        if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
-            return false;
-
-        var found = false;
-        var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(targetGrid);
-        var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
-
-        for (var i = 0; i < 10; i++)
-        {
-            var randomX = RobustRandom.Next((int) gridBounds.Left, (int) gridBounds.Right);
-            var randomY = RobustRandom.Next((int) gridBounds.Bottom, (int) gridBounds.Top);
-
-            tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y);
-            if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile,
-                    mapGridComp: gridComp)
-                || _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp))
-            {
-                continue;
-            }
-
-            found = true;
-            targetCoords = gridComp.GridTileToLocal(tile);
-            break;
-        }
-
-        return found;
-    }
     public float GetSeverityModifier()
     {
         var ev = new GetSeverityModifierEvent();
index 96fb9f9f405f3de1fbbb30871547fe999ef28147..792459c72f72fce7dabcae88702dc655e91bbec1 100644 (file)
@@ -12,14 +12,12 @@ namespace Content.Shared.Storage;
 [DataDefinition]
 public partial struct EntitySpawnEntry
 {
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string? PrototypeId = null;
+    [DataField("id")]
+    public EntProtoId? PrototypeId = null;
 
     /// <summary>
     ///     The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField("prob")] public float SpawnProbability = 1;
 
     /// <summary>
@@ -43,19 +41,16 @@ public partial struct EntitySpawnEntry
     /// </code>
     ///     </example>
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField("orGroup")] public string? GroupId = null;
 
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("amount")] public int Amount = 1;
+    [DataField] public int Amount = 1;
 
     /// <summary>
     ///     How many of this can be spawned, in total.
     ///     If this is lesser or equal to <see cref="Amount"/>, it will spawn <see cref="Amount"/> exactly.
     ///     Otherwise, it chooses a random value between <see cref="Amount"/> and <see cref="MaxAmount"/> on spawn.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("maxAmount")] public int MaxAmount = 1;
+    [DataField] public int MaxAmount = 1;
 
     public EntitySpawnEntry() { }
 }
index 40f6bf6dbed5631b475883af7fe88280d3bf4a81..0c7e4cb8b24924df37eafbb0a5b0af040a21f755 100644 (file)
     lightRadius: 6
     lightSoftness: 1.1
 
+- type: entity
+  parent: LightBulb
+  name: old incandescent light bulb
+  id: LightBulbOld
+  description: An aging light bulb.
+  components:
+  - type: LightBulb
+    bulb: Bulb
+    color: "#FFD1A3" # 4000K color temp
+    lightEnergy: 0.3 # old incandescents just arent as bright
+    lightRadius: 6
+    lightSoftness: 1.1
+
 - type: entity
   suffix: Broken
   parent: BaseLightbulb
     lightSoftness: 1
     PowerUse: 25
 
+- type: entity
+  parent: LightTube
+  name: old fluorescent light tube
+  id: LightTubeOld
+  description: An aging light fixture.
+  components:
+  - type: LightBulb
+    color: "#FFDABB" # old fluorescents are yellower-4500K temp
+    lightEnergy: 0.5
+    lightRadius: 10
+    lightSoftness: 1
+    PowerUse: 10
+
 - type: entity
   suffix: Broken
   parent: BaseLightTube
index 570e5fbbf7a9c3e81c7d456a475b058b8eebec13..a62fcb36a951263a802b972b04d772ebdba6505e 100644 (file)
           3: { state: reinf_construct-3, visible: true}
           4: { state: reinf_construct-4, visible: true}
           5: { state: reinf_construct-5, visible: true}
+  - type: ReinforcedWallReplacementMarker
   - type: StaticPrice
     price: 150
   - type: RadiationBlocker
       - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/solid.rsi
+  - type: WallReplacementMarker
   - type: Construction
     graph: Girder
     node: wall
index 7d1128a416eb83e63e20d4beae5c0b9782d19406..bffaae6ab492d6de33f89beb4130d45acb5c8278 100644 (file)
   noSpawn: true
   components:
   - type: RampingStationEventScheduler
+
+# variation passes
+- type: entity
+  id: BasicRoundstartVariation
+  parent: BaseGameRule
+  noSpawn: true
+  components:
+  - type: RoundstartStationVariationRule
+    rules:
+    - id: BasicPoweredLightVariationPass
+    - id: BasicTrashVariationPass
+    - id: SolidWallRustingVariationPass
+    - id: ReinforcedWallRustingVariationPass
+    - id: BasicPuddleMessVariationPass
+      prob: 0.99
+      orGroup: puddleMess
+    - id: BloodbathPuddleMessVariationPass
+      prob: 0.01
+      orGroup: puddleMess
diff --git a/Resources/Prototypes/GameRules/variation.yml b/Resources/Prototypes/GameRules/variation.yml
new file mode 100644 (file)
index 0000000..bb9649f
--- /dev/null
@@ -0,0 +1,120 @@
+# Base
+
+- type: entity
+  id: BaseVariationPass
+  parent: BaseGameRule
+  abstract: true
+  noSpawn: true
+  components:
+  - type: StationVariationPassRule
+
+# Actual rules
+
+- type: entity
+  id: BasicPoweredLightVariationPass
+  parent: BaseVariationPass
+  noSpawn: true
+  components:
+  - type: PoweredLightVariationPass
+
+- type: entity
+  id: SolidWallRustingVariationPass
+  parent: BaseVariationPass
+  noSpawn: true
+  components:
+  - type: WallReplaceVariationPass
+  - type: EntityReplaceVariationPass
+    entitiesPerReplacementAverage: 10
+    entitiesPerReplacementStdDev: 2
+    replacements:
+    - id: WallSolidRust
+
+- type: entity
+  id: ReinforcedWallRustingVariationPass
+  parent: BaseVariationPass
+  noSpawn: true
+  components:
+  - type: ReinforcedWallReplaceVariationPass
+  - type: EntityReplaceVariationPass
+    entitiesPerReplacementAverage: 12
+    entitiesPerReplacementStdDev: 2
+    replacements:
+    - id: WallReinforcedRust
+
+- type: entity
+  id: BasicTrashVariationPass
+  parent: BaseVariationPass
+  noSpawn: true
+  components:
+  - type: EntitySpawnVariationPass
+    tilesPerEntityAverage: 35
+    tilesPerEntityStdDev: 4
+    entities:
+    - id: RandomSpawner
+
+- type: weightedRandomFillSolution
+  id: RandomFillTrashPuddle
+  fills:
+  - quantity: 80
+    weight: 5
+    reagents:
+    - Vomit
+    - InsectBlood
+    - WeldingFuel
+    - Mold
+  - quantity: 55
+    weight: 4
+    reagents:
+    - PlantBGone
+    - Potassium # :trollface:
+    - VentCrud
+    - Carbon
+    - Hydrogen
+    - Fat
+    - SpaceLube
+    - SpaceGlue
+    - Sulfur
+    - Acetone
+    - Bleach
+  - quantity: 40
+    weight: 3
+    reagents:
+    - Blood
+    - CopperBlood
+    - Slime
+  - quantity: 25
+    weight: 1
+    reagents:
+    - Omnizine
+    - Desoxyephedrine
+    - Napalm
+    - Radium
+    - Gold
+    - Silver
+    - Sodium
+
+- type: weightedRandomFillSolution
+  id: RandomFillTrashPuddleBloodbath
+  fills:
+  - quantity: 80
+    weight: 1
+    reagents:
+    - Blood
+
+- type: entity
+  id: BasicPuddleMessVariationPass
+  parent: BaseVariationPass
+  noSpawn: true
+  components:
+  - type: PuddleMessVariationPass
+    randomPuddleSolutionFill: RandomFillTrashPuddle
+
+- type: entity
+  id: BloodbathPuddleMessVariationPass
+  parent: BaseVariationPass
+  noSpawn: true
+  components:
+  - type: PuddleMessVariationPass
+    tilesPerSpillAverage: 150
+    tilesPerSpillStdDev: 10
+    randomPuddleSolutionFill: RandomFillTrashPuddleBloodbath
index 205d12550fd4df7da7b393dd914c75f8da8230c6..ae1adcf80d61daaed64b9e5c942a16446820e285 100644 (file)
@@ -7,6 +7,7 @@
   description: survival-description
   rules:
     - RampingStationEventScheduler
+    - BasicRoundstartVariation
 
 - type: gamePreset
   id: AllAtOnce
@@ -30,6 +31,7 @@
   description: extended-description
   rules:
   - BasicStationEventScheduler
+  - BasicRoundstartVariation
 
 - type: gamePreset
   id: Greenshift
@@ -39,6 +41,8 @@
   name: greenshift-title
   showInVote: false #4boring4vote
   description: greenshift-description
+  rules:
+  - BasicRoundstartVariation
 
 - type: gamePreset
   id: Secret
@@ -72,6 +76,7 @@
   rules:
     - Traitor
     - BasicStationEventScheduler
+    - BasicRoundstartVariation
 
 - type: gamePreset
   id: Deathmatch
   rules:
     - Nukeops
     - BasicStationEventScheduler
+    - BasicRoundstartVariation
 
 - type: gamePreset
   id: Revolutionary
   rules:
     - Revolutionary
     - BasicStationEventScheduler
+    - BasicRoundstartVariation
 
 - type: gamePreset
   id: Zombie
   rules:
   - Zombie
   - BasicStationEventScheduler
+  - BasicRoundstartVariation
 
 - type: gamePreset
   id: Pirates
   rules:
     - Pirates
     - BasicStationEventScheduler
+    - BasicRoundstartVariation