Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Content.Server.StationEvents.Components;

/// <summary>
/// A station event scheduler that emits events at irregular intervals, with occasional chaos and occasional calmness.
/// </summary>
[RegisterComponent]
public sealed partial class OscillatingStationEventSchedulerComponent : Component
{
// TODO cvars?
[DataField]
public float MinChaos = 0.1f, MaxChaos = 15f;

/// <summary>
/// The amount of chaos at the beginning of the round.
/// </summary>
[DataField]
public float StartingChaosRatio = 0f;

/// <summary>
/// The value of the first derivative of the event delay function at the beginning of the shift.
/// Must be between 1 and -1.
/// </summary>
[DataField]
public float StartingSlope = 1f;

/// <summary>
/// Biases that determine how likely the event rate is to go up or down, and how fast it's going to happen.
/// </summary>
/// <remarks>
/// Downwards bias must always be negative, and upwards must be positive. Otherwise, you'll get odd behavior or errors.
/// </remarks>
[DataField]
public float DownwardsBias = -1f, UpwardsBias = 1f;

/// <summary>
/// Limits that define how large the chaos slope can become.
/// </summary>
/// <remarks>
/// Downwards limit must always be negative, and upwards must be positive. Otherwise, you'll get odd behavior or errors.
/// </remarks>
[DataField]
public float DownwardsLimit = -1f, UpwardsLimit = 1f;

/// <summary>
/// A value between 0 and 1 that determines how slowly the chaos and its first derivative change in time.
/// </summary>
/// <remarks>
/// Changing these values will have a great impact on how fast the event rate changes.
/// </remarks>
[DataField]
public float ChaosStickiness = 0.93f, SlopeStickiness = 0.96f;

/// <summary>
/// Actual chaos data at the current moment. Those are overridden at runtime.
/// </summary>
[DataField]
public float CurrentChaos, CurrentSlope, LastAcceleration;


[DataField]
public TimeSpan NextUpdate = TimeSpan.Zero, LastEventTime = TimeSpan.Zero;

/// <summary>
/// Update interval, which determines how often current chaos is recalculated.
/// Modifying this value does not directly impact the event rate, but changes how stable the slope is.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(5f);
}
102 changes: 102 additions & 0 deletions Content.Server/StationEvents/OscillatingStationEventScheduler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.StationEvents.Components;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

namespace Content.Server.StationEvents;

public sealed class OscillatingStationEventSchedulerSystem : GameRuleSystem<OscillatingStationEventSchedulerComponent>
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EventManagerSystem _event = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IGameTiming _timing = default!;

[Conditional("DEBUG")]
private void DebugValidateParams(OscillatingStationEventSchedulerComponent c)
{
// This monstrousity is necessary because if someone fucks up one of these parameters,
// it will likely either crash the game (in debug), or cause the event scheduler to stop working and spam the server console (in prod)
DebugTools.Assert(c.DownwardsBias <= 0f && c.UpwardsBias >= 0f, "Fix your scheduler bias!");
DebugTools.Assert(c.DownwardsLimit <= 0f && c.UpwardsLimit >= 0f, "Fix your scheduler slope limits!");
DebugTools.Assert(c.UpdateInterval > TimeSpan.Zero, "scheduler update interval must be positive!");
DebugTools.Assert(c.ChaosStickiness >= 0f && c.ChaosStickiness <= 1f, "Scheduler stickiness must be between 0 and 1!");
DebugTools.Assert(c.SlopeStickiness >= 0f && c.SlopeStickiness <= 1f, "Scheduler stickiness must be between 0 and 1!");
DebugTools.Assert(c.MinChaos < c.MaxChaos, "What did you expect??");
}

private TimeSpan CalculateAverageEventTime(OscillatingStationEventSchedulerComponent comp)
{
// TODO those cvars are bad
var min = _cfg.GetCVar(CCVars.GameEventsOscillatingMinimumTime);
var max = _cfg.GetCVar(CCVars.GameEventsOscillatingAverageTime);

return TimeSpan.FromSeconds(min + (max - min) / comp.CurrentChaos); // Why does C# have no math.lerp??????????????
}

protected override void Started(EntityUid uid, OscillatingStationEventSchedulerComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
DebugValidateParams(comp);

comp.CurrentChaos = comp.MinChaos + comp.StartingChaosRatio * (comp.MaxChaos - comp.MinChaos);
comp.CurrentSlope = comp.StartingSlope;

comp.NextUpdate = _timing.CurTime + CalculateAverageEventTime(comp);
comp.LastEventTime = _timing.CurTime; // just so we don't run an event the very moment this scheduler gets added
}

protected override void ActiveTick(EntityUid uid, OscillatingStationEventSchedulerComponent comp, GameRuleComponent gameRule, float frameTime)
{
if (comp.NextUpdate > _timing.CurTime)
return;
comp.NextUpdate = _timing.CurTime + comp.UpdateInterval;
DebugValidateParams(comp);

// Slope is the first derivative of chaos, and acceleration is the second
// We randomize acceleration on each tick and simulate its effect on the slope and base function
// But we spread the effect across a longer time span to achieve a smooth and pleasant result
var delta = (float) comp.UpdateInterval.TotalSeconds;
var newAcceleration = _random.NextFloat(comp.DownwardsBias, comp.UpwardsBias);
var newSlope =
Math.Clamp(comp.CurrentSlope + newAcceleration * delta, comp.DownwardsLimit, comp.UpwardsLimit) * (1 - comp.SlopeStickiness)
+ comp.CurrentSlope * comp.SlopeStickiness;
var newChaos =
Math.Clamp(comp.CurrentChaos + newSlope * delta, comp.MinChaos, comp.MaxChaos) * (1 - comp.ChaosStickiness)
+ comp.CurrentChaos * comp.ChaosStickiness;

comp.CurrentChaos = newChaos;
comp.CurrentSlope = newSlope;
comp.LastAcceleration = newAcceleration;

// We do not use fixed "next event" times because that can cause us to skip over chaos spikes due to periods of low chaos
// Instead we recalculate the time until next event every time, so it can change before the event is even started
var targetDelay = CalculateAverageEventTime(comp);
if (_timing.CurTime > comp.LastEventTime + targetDelay && TryRunNextEvent(uid, comp, out _))
{
#if DEBUG
var passed = _timing.CurTime - comp.LastEventTime;
Log.Debug($"Running an event after {passed.TotalSeconds} sec since last event. Next event scheduled in {CalculateAverageEventTime(comp).TotalSeconds} sec.");
#endif

comp.LastEventTime = _timing.CurTime;
}
}

public bool TryRunNextEvent(EntityUid uid, OscillatingStationEventSchedulerComponent comp, [NotNullWhen(true)] out string? runEvent)
{
runEvent = _event.PickRandomEvent();
if (runEvent == null)
return false;

_gameTicker.AddGameRule(runEvent);
return true;
}
}
23 changes: 16 additions & 7 deletions Content.Shared/CCVar/CCVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,28 +178,37 @@ public static readonly CVarDef<bool>
/// Minimum time between Basic station events in seconds
/// </summary>
public static readonly CVarDef<int> // 5 Minutes
GameEventsBasicMinimumTime = CVarDef.Create("game.events_basic_minimum_time", 300, CVar.SERVERONLY);
GameEventsBasicMinimumTime = CVarDef.Create("game.events_basic_minimum_time", 300, CVar.SERVERONLY | CVar.ARCHIVE);

/// <summary>
/// Maximum time between Basic station events in seconds
/// </summary>
public static readonly CVarDef<int> // 25 Minutes
GameEventsBasicMaximumTime = CVarDef.Create("game.events_basic_maximum_time", 1500, CVar.SERVERONLY);
GameEventsBasicMaximumTime = CVarDef.Create("game.events_basic_maximum_time", 1500, CVar.SERVERONLY | CVar.ARCHIVE);

/// <summary>
/// Minimum time between Ramping station events in seconds
/// </summary>
public static readonly CVarDef<int> // 4 Minutes
GameEventsRampingMinimumTime = CVarDef.Create("game.events_ramping_minimum_time", 240, CVar.SERVERONLY);
GameEventsRampingMinimumTime = CVarDef.Create("game.events_ramping_minimum_time", 240, CVar.SERVERONLY | CVar.ARCHIVE);

/// <summary>
/// Maximum time between Ramping station events in seconds
/// </summary>
public static readonly CVarDef<int> // 12 Minutes
GameEventsRampingMaximumTime = CVarDef.Create("game.events_ramping_maximum_time", 720, CVar.SERVERONLY);
GameEventsRampingMaximumTime = CVarDef.Create("game.events_ramping_maximum_time", 720, CVar.SERVERONLY | CVar.ARCHIVE);

/// <summary>
///
/// Minimum time between Oscillating station events in seconds. This is the bare minimum which will never be violated, unlike with ramping events.
/// </summary>
public static readonly CVarDef<int> // 40 seconds
GameEventsOscillatingMinimumTime = CVarDef.Create("game.events_oscillating_minimum_time", 40, CVar.SERVERONLY | CVar.ARCHIVE);

/// <summary>
/// Time between Oscillating station events in seconds at 1x chaos level. Events may occur at larger intervals if current chaos is lower than that.
/// </summary>
public static readonly CVarDef<int> // 20 Minutes - which constitutes a minimum of 120 seconds between events in Irregular and 280 seconds in Extended Irregular
GameEventsOscillatingAverageTime = CVarDef.Create("game.events_oscillating_average_time", 1200, CVar.SERVERONLY | CVar.ARCHIVE);

/// <summary>
/// Controls the maximum number of character slots a player is allowed to have.
Expand Down Expand Up @@ -2291,7 +2300,7 @@ public static readonly CVarDef<float>
/// </summary>
public static readonly CVarDef<float> StationGoalsChance =
CVarDef.Create("game.station_goals_chance", 0.1f, CVar.SERVERONLY);


#region CPR System
/// <summary>
Expand Down Expand Up @@ -2338,7 +2347,7 @@ public static readonly CVarDef<float>
/// </summary>
public static readonly CVarDef<float> CPRAirlossReductionMultiplier =
CVarDef.Create("cpr.airloss_reduction_multiplier", 1f, CVar.REPLICATED | CVar.SERVER);

#endregion

#region Contests System
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
irregular-title = Irregular
irregular-description = Threat level varies throughout the shift. Sometimes it's a paradise, sometimes it's a disaster.

irregular-extended-title = Irregular Extended
irregular-extended-description = A rather calm experience with occasional spikes of threats.
27 changes: 27 additions & 0 deletions Resources/Prototypes/GameRules/roundstart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,33 @@
startingChaosRatio: 0.025 # Starts as slow as survival, but quickly ramps up
shiftLengthModifier: 2.5

- type: entity
id: IrregularStationEventScheduler
parent: BaseGameRule
noSpawn: true
components:
- type: OscillatingStationEventScheduler
minChaos: 0.8
maxChaos: 14
startingSlope: 0.2
downwardsLimit: -0.35
upwardsLimit: 0.4

# More likely to go down than up, so calmness prevails
- type: entity
id: IrregularExtendedStationEventScheduler
parent: BaseGameRule
noSpawn: true
components:
- type: OscillatingStationEventScheduler
minChaos: 0.8
maxChaos: 8
startingSlope: -1
downwardsLimit: -0.4
upwardsLimit: 0.3
downwardsBias: -1.1
upwardsBias: 0.9

# variation passes
- type: entity
id: BasicRoundstartVariation
Expand Down
20 changes: 20 additions & 0 deletions Resources/Prototypes/game_presets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@
- HellshiftStationEventScheduler
- BasicRoundstartVariation

- type: gamePreset
id: SurvivalIrregular
alias: [irregular]
showInVote: true
name: irregular-title
description: irregular-description
rules:
- IrregularStationEventScheduler
- BasicRoundstartVariation

- type: gamePreset
id: SurvivalIrregularExtended
alias: [irregular-extended]
showInVote: true
name: irregular-extended-title
description: irregular-extended-description
rules:
- IrregularExtendedStationEventScheduler
- BasicRoundstartVariation

- type: gamePreset
id: AllAtOnce
name: all-at-once-title
Expand Down