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
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))
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;
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);
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.
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;
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);
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)
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;
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))
return true;
}
- private void StartGamePresetRules()
+ public void StartGamePresetRules()
{
// May be touched by the preset during init.
var rules = new List<EntityUid>(GetAddedGameRules());
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);
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);
--- /dev/null
+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();
+}
--- /dev/null
+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
+{
+}
--- /dev/null
+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;
+ }
+
+}
+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();
}
- 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);
--- /dev/null
+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);
[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);
}
// 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);
}
}
--- /dev/null
+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));
+ }
+ }
+}
--- /dev/null
+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!;
+}
--- /dev/null
+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!;
+}
--- /dev/null
+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";
+}
--- /dev/null
+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!;
+}
--- /dev/null
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+
+[RegisterComponent]
+public sealed partial class ReinforcedWallReplaceVariationPassComponent : Component
+{
+}
--- /dev/null
+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
+{
+}
--- /dev/null
+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
+{
+}
--- /dev/null
+namespace Content.Server.GameTicking.Rules.VariationPass.Components;
+
+
+[RegisterComponent]
+public sealed partial class WallReplaceVariationPassComponent : Component
+{
+}
--- /dev/null
+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);
+ }
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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>
+{
+}
--- /dev/null
+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);
+}
--- /dev/null
+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>
+{
+}
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
/// </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))
--- /dev/null
+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
+{
+}
+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);
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;
[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!;
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>
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;
}
[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!;
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();
[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>
/// </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() { }
}
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
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
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
--- /dev/null
+# 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
description: survival-description
rules:
- RampingStationEventScheduler
+ - BasicRoundstartVariation
- type: gamePreset
id: AllAtOnce
description: extended-description
rules:
- BasicStationEventScheduler
+ - BasicRoundstartVariation
- type: gamePreset
id: Greenshift
name: greenshift-title
showInVote: false #4boring4vote
description: greenshift-description
+ rules:
+ - BasicRoundstartVariation
- type: gamePreset
id: Secret
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