diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 941337f7ed..0f665a63de 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -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 diff --git a/Content.Server/Chemistry/Containers/EntitySystems/SolutionContainerSystem.cs b/Content.Server/Chemistry/Containers/EntitySystems/SolutionContainerSystem.cs index fcf68013fa..7926121c2b 100644 --- a/Content.Server/Chemistry/Containers/EntitySystems/SolutionContainerSystem.cs +++ b/Content.Server/Chemistry/Containers/EntitySystems/SolutionContainerSystem.cs @@ -30,10 +30,10 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys public Solution EnsureSolution(Entity entity, string name, out bool existed) => EnsureSolution(entity, name, FixedPoint2.Zero, out existed); - public Solution EnsureSolution(Entity entity, string name, FixedPoint2 minVol, out bool existed) - => EnsureSolution(entity, name, minVol, null, out existed); + public Solution EnsureSolution(Entity entity, string name, FixedPoint2 maxVol, out bool existed) + => EnsureSolution(entity, name, maxVol, null, out existed); - public Solution EnsureSolution(Entity entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed) + public Solution EnsureSolution(Entity 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(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 EnsureSolutionEntity(Entity entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed) + public void EnsureAllSolutions(Entity 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 EnsureSolutionEntity(Entity 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 entity, string name, FixedPoint2 minVol, Solution? prototype, out bool existed) + private Solution EnsureSolutionPrototype(Entity 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 SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 minVol, Solution prototype) + private Entity 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 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 entity, ref ComponentShutdown args) diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index c5eb42e05b..b6df3a171b 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -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)) diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index 04f7be016a..b97a16ab99 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -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(GetAddedGameRules()); diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 971e103c1b..4ebe946af4 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -141,6 +141,24 @@ public sealed partial class GameTicker return true; } + /// + /// Returns true if a game rule with the given component has been added. + /// + public bool IsGameRuleAdded() + where T : IComponent + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out _)) + { + if (HasComp(uid)) + continue; + + return true; + } + + return false; + } + public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null) { return Resolve(ruleEntity, ref component) && !HasComp(ruleEntity); @@ -157,6 +175,22 @@ public sealed partial class GameTicker return false; } + /// + /// Returns true if a game rule with the given component is active.. + /// + public bool IsGameRuleActive() + where T : IComponent + { + var query = EntityQueryEnumerator(); + // 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(ruleEntity); diff --git a/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs new file mode 100644 index 0000000000..44ae89f7b3 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/RoundstartStationVariationRuleComponent.cs @@ -0,0 +1,19 @@ +using Content.Shared.Storage; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// This handles starting various roundstart variation rules after a station has been loaded. +/// +[RegisterComponent] +public sealed partial class RoundstartStationVariationRuleComponent : Component +{ + /// + /// The list of rules that will be started once the map is spawned. + /// Uses to support probabilities for various rules + /// without having to hardcode the probability directly in the rule's logic. + /// + [DataField(required: true)] + public List Rules = new(); +} diff --git a/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs new file mode 100644 index 0000000000..9fdc62a7e0 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/StationVariationPassRuleComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// This is a marker component placed on rule entities which are a single "pass" of station variation. +/// +[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 index 0000000000..9d75e65472 --- /dev/null +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -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 where T: IComponent +{ + protected EntityQueryEnumerator QueryActiveRules() + { + return EntityQueryEnumerator(); + } + + protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName) + { + var query = EntityQueryEnumerator(); + 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; + } + + /// + /// Utility function for finding a random event-eligible station entity + /// + protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func? filter = null) + { + var stations = new ValueList(Count()); + + filter ??= _ => true; + var query = AllEntityQuery(); + + 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(targetStation.Value)), + out tile, + out targetGrid, + out targetCoords); + } + + return false; + } + + protected bool TryFindRandomTileOnStation(Entity 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(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; + } + +} diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs index ba781e32e2..1667e73d04 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -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 : 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 : EntitySystem where T : ICompon } - protected EntityQueryEnumerator QueryActiveRules() - { - return EntityQueryEnumerator(); - } - - protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName) - { - var query = EntityQueryEnumerator(); - 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 index 0000000000..7755f684be --- /dev/null +++ b/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs @@ -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; + +/// +public sealed class RoundstartStationVariationRuleSystem : GameRuleSystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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()) + return; + + // this is unlikely, but could theoretically happen if it was saved and reloaded, so check anyway + if (HasComp(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(); + 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(uid)) + continue; + + RaiseLocalEvent(uid, ref passEv); + } + + EnsureComp(ev.Station); + } +} + +/// +/// Raised directed on game rule entities which are added and marked as +/// when a new station is initialized that should be varied. +/// +/// The new station that was added, and its config & grids. +[ByRefEvent] +public readonly record struct StationVariationPassEvent(Entity Station); diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs index 1e3858ceef..6a00eb7d10 100644 --- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs @@ -18,9 +18,9 @@ public sealed class SecretRuleSystem : GameRuleSystem [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 // but currently there's no way to know anyway as they use cvars. var presetString = _configurationManager.GetCVar(CCVars.SecretWeightPrototype); var preset = _prototypeManager.Index(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(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 index 0000000000..00b2546e78 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs @@ -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; + +/// +/// +/// 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 --each replaceable entity must have it +/// Then you need an inheriting system as well as a unique game rule component for +/// +/// This means a bit more boilerplate for each one, but significantly faster to actually execute. +/// See +/// +public abstract class BaseEntityReplaceVariationPassSystem : VariationPassSystem + where TEntComp: IComponent + where TGameRuleComp: IComponent +{ + /// + /// Used so we don't modify while enumerating + /// if the replaced entity also has . + /// + /// Filled and cleared within the same tick so no persistence issues. + /// + private readonly Queue<(string, EntityCoordinates, Angle)> _queuedSpawns = new(); + + protected override void ApplyVariation(Entity ent, ref StationVariationPassEvent args) + { + if (!TryComp(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(); + 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 ent, List 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 index 0000000000..6603251754 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/EntityReplaceVariationPassComponent.cs @@ -0,0 +1,33 @@ +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules.VariationPass.Components; + +/// +/// This is used for replacing a certain amount of entities with other entities in a variation pass. +/// +/// +/// +/// POTENTIALLY REPLACEABLE ENTITIES MUST BE MARKED WITH A REPLACEMENT MARKER +/// AND HAVE A SYSTEM INHERITING FROM +/// SEE +/// +[RegisterComponent] +public sealed partial class EntityReplaceVariationPassComponent : Component +{ + /// + /// Number of matching entities before one will be replaced on average. + /// + [DataField(required: true)] + public float EntitiesPerReplacementAverage; + + [DataField(required: true)] + public float EntitiesPerReplacementStdDev; + + /// + /// Prototype(s) to replace matched entities with. + /// + [DataField(required: true)] + public List 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 index 0000000000..f7ddd7ce9d --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/EntitySpawnVariationPassComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Random; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules.VariationPass.Components; + +/// +/// This is used for spawning entities randomly dotted around the station in a variation pass. +/// +[RegisterComponent] +public sealed partial class EntitySpawnVariationPassComponent : Component +{ + /// + /// Number of tiles before we spawn one entity on average. + /// + [DataField] + public float TilesPerEntityAverage = 50f; + + [DataField] + public float TilesPerEntityStdDev = 7f; + + /// + /// Spawn entries for each chosen location. + /// + [DataField(required: true)] + public List 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 index 0000000000..98c58e0a04 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/PoweredLightVariationPassComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.Light.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules.VariationPass.Components; + +/// +/// This handle randomly destroying lights, causing them to flicker endlessly, or replacing their tube/bulb with different variants. +/// +[RegisterComponent] +public sealed partial class PoweredLightVariationPassComponent : Component +{ + /// + /// Chance that a light will be replaced with a broken variant. + /// + [DataField] + public float LightBreakChance = 0.15f; + + /// + /// Chance that a light will be replaced with an aged variant. + /// + [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 index 0000000000..787d338652 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/PuddleMessVariationPassComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Random; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules.VariationPass.Components; + +/// +/// Handles spilling puddles with various reagents randomly around the station. +/// +[RegisterComponent] +public sealed partial class PuddleMessVariationPassComponent : Component +{ + /// + /// Tiles before one spill on average. + /// + [DataField] + public float TilesPerSpillAverage = 600f; + + [DataField] + public float TilesPerSpillStdDev = 50f; + + /// + /// Weighted random prototype to use for random messes. + /// + [DataField(required: true)] + public ProtoId 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 index 0000000000..82095fe224 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/ReinforcedWallReplaceVariationPassComponent.cs @@ -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 index 0000000000..00f55b7e25 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/ReinforcedWallReplacementMarkerComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers; + +/// +/// This component marks replaceable reinforced walls for use with fast queries in variation passes. +/// +[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 index 0000000000..6df0432198 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/WallReplacementMarkerComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers; + +/// +/// This component marks replaceable walls for use with fast queries in variation passes. +/// +[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 index 0000000000..3598deeb26 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/Components/WallReplaceVariationPassComponent.cs @@ -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 index 0000000000..7247bd98aa --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs @@ -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; + +/// +public sealed class EntitySpawnVariationPassSystem : VariationPassSystem +{ + protected override void ApplyVariation(Entity 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 index 0000000000..dae6981f95 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/PoweredLightVariationPassSystem.cs @@ -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; + +/// +public sealed class PoweredLightVariationPassSystem : VariationPassSystem +{ + [Dependency] private readonly PoweredLightSystem _poweredLight = default!; + + protected override void ApplyVariation(Entity ent, ref StationVariationPassEvent args) + { + var query = AllEntityQuery(); + 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 index 0000000000..41cdbd87f8 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs @@ -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; + +/// +public sealed class PuddleMessVariationPassSystem : VariationPassSystem +{ + [Dependency] private readonly PuddleSystem _puddle = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + protected override void ApplyVariation(Entity 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 index 0000000000..1950581bea --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/ReinforcedWallReplaceVariationPassSystem.cs @@ -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; + +/// +/// This handles the ability to replace entities marked with in a variation pass +/// +public sealed class ReinforcedWallReplaceVariationPassSystem : BaseEntityReplaceVariationPassSystem +{ +} diff --git a/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs new file mode 100644 index 0000000000..b6ead21479 --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/VariationPassSystem.cs @@ -0,0 +1,29 @@ +using Content.Server.Station.Systems; +using Robust.Shared.Random; + +namespace Content.Server.GameTicking.Rules.VariationPass; + +/// +/// 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. +/// +public abstract class VariationPassSystem : GameRuleSystem + where T: IComponent +{ + [Dependency] protected readonly StationSystem Stations = default!; + [Dependency] protected readonly IRobustRandom Random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(ApplyVariation); + } + + protected bool IsMemberOfStation(Entity ent, ref StationVariationPassEvent args) + { + return Stations.GetOwningStation(ent, ent.Comp) == args.Station.Owner; + } + + protected abstract void ApplyVariation(Entity 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 index 0000000000..e9777d829d --- /dev/null +++ b/Content.Server/GameTicking/Rules/VariationPass/WallReplaceVariationPassSystem.cs @@ -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; + +/// +/// This handles the ability to replace entities marked with in a variation pass +/// +public sealed class WallReplaceVariationPassSystem : BaseEntityReplaceVariationPassSystem +{ +} diff --git a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs index e6cde4495c..ca44b1a4c9 100644 --- a/Content.Server/Light/EntitySystems/PoweredLightSystem.cs +++ b/Content.Server/Light/EntitySystems/PoweredLightSystem.cs @@ -214,6 +214,21 @@ namespace Content.Server.Light.EntitySystems return bulb; } + /// + /// Replaces the spawned prototype of a pre-mapinit powered light with a different variant. + /// + public bool ReplaceSpawnedPrototype(Entity 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; + } + /// /// 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 /// 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 index 0000000000..65c794f3fc --- /dev/null +++ b/Content.Server/Station/Components/StationVariationHasRunComponent.cs @@ -0,0 +1,12 @@ +using Content.Server.GameTicking.Rules; + +namespace Content.Server.Station.Components; + +/// +/// Marker component for stations where procedural variation using +/// has already run, so as to avoid running it again if another station is added. +/// +[RegisterComponent] +public sealed partial class StationVariationHasRunComponent : Component +{ +} diff --git a/Content.Server/Station/Events/StationPostInitEvent.cs b/Content.Server/Station/Events/StationPostInitEvent.cs index a4e55fafb2..4f7927cee5 100644 --- a/Content.Server/Station/Events/StationPostInitEvent.cs +++ b/Content.Server/Station/Events/StationPostInitEvent.cs @@ -1,7 +1,9 @@ +using Content.Server.Station.Components; + namespace Content.Server.Station.Events; /// -/// Raised directed on a station after it has been initialized. +/// Raised directed on a station after it has been initialized, as well as broadcast. /// [ByRefEvent] -public readonly record struct StationPostInitEvent; +public readonly record struct StationPostInitEvent(Entity Station); diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/StationSystem.cs index a0adeb2243..be3fc57967 100644 --- a/Content.Server/Station/Systems/StationSystem.cs +++ b/Content.Server/Station/Systems/StationSystem.cs @@ -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; } + /// + /// Returns the total number of tiles contained in the station's grids. + /// + public int GetTileCount(StationDataComponent component) + { + var count = 0; + foreach (var gridUid in component.Grids) + { + if (!TryComp(gridUid, out var grid)) + continue; + + count += _map.GetAllTiles(gridUid, grid).Count(); + } + + return count; + } + /// /// Tries to retrieve a filter for everything in the station the source is on. /// @@ -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; } diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index 537a7e7c22..221beccee7 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -29,7 +29,6 @@ public abstract partial class StationEventSystem : GameRuleSystem 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 : GameRuleSystem where T GameTicker.EndGameRule(uid, component); } - protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func? filter = null) - { - var stations = new ValueList(Count()); - - filter ??= _ => true; - var query = AllEntityQuery(); - - 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(targetStation.Value).Grids; - if (possibleTargets.Count == 0) - { - targetGrid = EntityUid.Invalid; - return false; - } - - targetGrid = RobustRandom.Pick(possibleTargets); - - if (!TryComp(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(); diff --git a/Content.Shared/Storage/EntitySpawnEntry.cs b/Content.Shared/Storage/EntitySpawnEntry.cs index 96fb9f9f40..792459c72f 100644 --- a/Content.Shared/Storage/EntitySpawnEntry.cs +++ b/Content.Shared/Storage/EntitySpawnEntry.cs @@ -12,14 +12,12 @@ namespace Content.Shared.Storage; [DataDefinition] public partial struct EntitySpawnEntry { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? PrototypeId = null; + [DataField("id")] + public EntProtoId? PrototypeId = null; /// /// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc. /// - [ViewVariables(VVAccess.ReadWrite)] [DataField("prob")] public float SpawnProbability = 1; /// @@ -43,19 +41,16 @@ public partial struct EntitySpawnEntry /// /// /// - [ViewVariables(VVAccess.ReadWrite)] [DataField("orGroup")] public string? GroupId = null; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("amount")] public int Amount = 1; + [DataField] public int Amount = 1; /// /// How many of this can be spawned, in total. /// If this is lesser or equal to , it will spawn exactly. /// Otherwise, it chooses a random value between and on spawn. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("maxAmount")] public int MaxAmount = 1; + [DataField] public int MaxAmount = 1; public EntitySpawnEntry() { } } diff --git a/Resources/Prototypes/Entities/Objects/Power/lights.yml b/Resources/Prototypes/Entities/Objects/Power/lights.yml index 40f6bf6dbe..0c7e4cb8b2 100644 --- a/Resources/Prototypes/Entities/Objects/Power/lights.yml +++ b/Resources/Prototypes/Entities/Objects/Power/lights.yml @@ -127,6 +127,19 @@ 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 @@ -164,6 +177,19 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml index 570e5fbbf7..a62fcb36a9 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml @@ -545,6 +545,7 @@ 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 @@ -840,6 +841,7 @@ - RCDDeconstructWhitelist - type: Sprite sprite: Structures/Walls/solid.rsi + - type: WallReplacementMarker - type: Construction graph: Girder node: wall diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 7d1128a416..bffaae6ab4 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -123,3 +123,22 @@ 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 index 0000000000..bb9649fddd --- /dev/null +++ b/Resources/Prototypes/GameRules/variation.yml @@ -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 diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index 205d12550f..ae1adcf80d 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -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 @@ -96,6 +101,7 @@ rules: - Nukeops - BasicStationEventScheduler + - BasicRoundstartVariation - type: gamePreset id: Revolutionary @@ -109,6 +115,7 @@ rules: - Revolutionary - BasicStationEventScheduler + - BasicRoundstartVariation - type: gamePreset id: Zombie @@ -124,6 +131,7 @@ rules: - Zombie - BasicStationEventScheduler + - BasicRoundstartVariation - type: gamePreset id: Pirates @@ -135,3 +143,4 @@ rules: - Pirates - BasicStationEventScheduler + - BasicRoundstartVariation