Add game.time_scale cvar

Primary use case (other than silly) is to be a better way to speed up/slow down replays.
This commit is contained in:
PJB3005
2025-12-20 16:32:06 +01:00
parent 2e5856b54d
commit 9b02a4e718
8 changed files with 74 additions and 5 deletions

View File

@@ -40,6 +40,7 @@ END TEMPLATE-->
### New features
* Added `IsUiOpen` and `IsAnyUiOpen` to `SharedUserInterfaceSystem`. (was in previous engine release, missed in changelog)
* Added `game.time_scale` CVar.
### Bugfixes

View File

@@ -61,6 +61,7 @@ namespace Robust.Client
NetMessageAccept.Handshake | NetMessageAccept.Client);
_configManager.OnValueChanged(CVars.NetTickrate, TickRateChanged, invokeImmediately: true);
_configManager.OnValueChanged(CVars.GameTimeScale, TimeScaleChanged, invokeImmediately: true);
_playMan.Initialize(0);
_playMan.PlayerListUpdated += OnPlayerListUpdated;
@@ -95,6 +96,18 @@ namespace Robust.Client
_logger.Info($"Tickrate changed to: {tickrate} on tick {_timing.CurTick}");
}
private void TimeScaleChanged(float timeScale, in CVarChangeInfo info)
{
if (!GameTiming.IsTimescaleValid(timeScale))
{
_logger.Error($"Invalid time scale set: {timeScale}, ignoring");
return;
}
_timing.TimeScale = timeScale;
_logger.Info($"Tickrate changed to: {timeScale} on tick {_timing.CurTick}");
}
/// <inheritdoc />
public void ConnectToServer(DnsEndPoint endPoint)
{

View File

@@ -440,7 +440,7 @@ namespace Robust.Client.GameStates
// If we are about to process an another tick in the same frame, lets not bother unnecessarily running prediction ticks
// Really the main-loop ticking just needs to be more specialized for clients.
if (_timing.TickRemainder >= _timing.CalcAdjustedTickPeriod())
if (_timing.TickRemainderRealtime >= _timing.CalcAdjustedTickPeriod())
return;
if (!processedAny)
@@ -467,7 +467,8 @@ namespace Robust.Client.GameStates
DebugTools.Assert(_timing.InSimulation);
var ping = (_network.ServerChannel?.Ping ?? 0) / 1000f + PredictLagBias; // seconds.
var predictionTarget = _timing.LastProcessedTick + (uint) (_processor.TargetBufferSize + Math.Ceiling(_timing.TickRate * ping) + PredictTickBias);
var lagTickCount = Math.Ceiling(_timing.TickRate * ping / _timing.TimeScale);
var predictionTarget = _timing.LastProcessedTick + (uint) (_processor.TargetBufferSize + lagTickCount + PredictTickBias);
if (IsPredictionEnabled)
{

View File

@@ -594,6 +594,19 @@ namespace Robust.Server
_logger.Info($"Tickrate changed to: {b} on tick {_time.CurTick}");
});
_config.OnValueChanged(CVars.GameTimeScale, f =>
{
if (!GameTiming.IsTimescaleValid(f))
{
_logger.Error($"Invalid time scale set: {f}, ignoring");
return;
}
_time.TimeScale = f;
_logger.Info($"Timescale changed to: {f} on tick {_time.CurTick}");
}, true);
var startOffset = TimeSpan.FromSeconds(_config.GetCVar(CVars.NetTimeStartOffset));
_time.TimeBase = (startOffset, GameTick.First);
_time.TickRate = (ushort) _config.GetCVar(CVars.NetTickrate);

View File

@@ -788,6 +788,12 @@ namespace Robust.Shared
public static readonly CVarDef<bool> GameAutoPauseEmpty =
CVarDef.Create("game.auto_pause_empty", true, CVar.SERVERONLY);
/// <summary>
/// Scales the game simulation time. Higher values make the game slower.
/// </summary>
public static readonly CVarDef<float> GameTimeScale =
CVarDef.Create("game.time_scale", 1f, CVar.REPLICATED | CVar.SERVER);
/*
* LOG
*/

View File

@@ -219,7 +219,7 @@ namespace Robust.Shared.Timing
if (_timing.Paused)
continue;
_timing.TickRemainder = accumulator;
_timing.TickRemainder = accumulator / _timing.TimeScale;
countTicksRan += 1;
// update the simulation
@@ -282,7 +282,7 @@ namespace Robust.Shared.Timing
// if not paused, save how close to the next tick we are so interpolation works
if (!_timing.Paused)
_timing.TickRemainder = accumulator;
_timing.TickRemainder = accumulator / _timing.TimeScale;
_timing.InSimulation = false;

View File

@@ -137,6 +137,8 @@ namespace Robust.Shared.Timing
set => SetTickRateAt(value, CurTick);
}
public float TimeScale { get; set; }
/// <summary>
/// The length of a tick at the current TickRate. 1/TickRate.
/// </summary>
@@ -156,13 +158,15 @@ namespace Robust.Shared.Timing
}
}
public TimeSpan TickRemainderRealtime => TickRemainder * TimeScale;
public TimeSpan CalcAdjustedTickPeriod()
{
// ranges from -1 to 1, with 0 being 'default'
var ratio = MathHelper.Clamp(TickTimingAdjustment, -0.99f, 0.99f);
// Final period ranges from near 0 (runs very fast to catch up) or 2 * tick period (runs at half speed).
return TickPeriod * (1-ratio);
return TickPeriod * (1-ratio) * TimeScale;
}
/// <summary>
@@ -302,5 +306,10 @@ namespace Robust.Shared.Timing
var variance = devSquared / (count - 1);
return TimeSpan.FromTicks((long)Math.Sqrt(variance));
}
internal static bool IsTimescaleValid(float scale)
{
return scale > 0 && float.IsNormal(scale) && float.IsFinite(scale);
}
}
}

View File

@@ -98,8 +98,19 @@ namespace Robust.Shared.Timing
/// <summary>
/// The target ticks/second of the simulation.
/// </summary>
/// <remarks>
/// This is specified in simulation time, not real time.
/// </remarks>
ushort TickRate { get; set; }
/// <summary>
/// The scale of simulation time to real time.
/// </summary>
/// <remarks>
/// A scale of 2 means the game should go "twice as slow"
/// </remarks>
float TimeScale { get; set; }
/// <summary>
/// The baseline time value that CurTime is calculated relatively to.
/// </summary>
@@ -108,6 +119,9 @@ namespace Robust.Shared.Timing
/// <summary>
/// The length of a tick at the current TickRate. 1/TickRate.
/// </summary>
/// <remarks>
/// This is in simulation time, not necessarily real time.
/// </remarks>
TimeSpan TickPeriod { get; }
/// <summary>
@@ -115,6 +129,18 @@ namespace Robust.Shared.Timing
/// </summary>
TimeSpan TickRemainder { get; set; }
/// <summary>
/// <see cref="TickRemainder"/> in real time.
/// </summary>
TimeSpan TickRemainderRealtime { get; }
/// <summary>
/// Calculate the amount of <b>real time</b> to wait between ticks.
/// </summary>
/// <remarks>
/// This is adjusted for various "out of simulation"
/// factors such as <see cref="TickTimingAdjustment"/> and <see cref="TimeScale"/>.
/// </remarks>
TimeSpan CalcAdjustedTickPeriod();
/// <summary>