Skip to content

Penalties System

Sirocco Race Timing supports three types of penalties:

  1. Time Penalties — add seconds to a competitor's race time (e.g., +5s)
  2. Lap Invalidations — mark laps as invalid so they don't count for best-lap purposes
  3. Disqualifications — remove a competitor from the results

Accessing the Penalty System

Get the managers via RaceTimingManager:

var penaltyManager = RaceTimingManager.Instance.LapTimer.RaceDirector.PenaltyManager;
var lapInvalidationManager = RaceTimingManager.Instance.LapTimer.RaceDirector.LapInvalidationManager;
var session = RaceTimingManager.Instance.LapTimer.Session; // needed for lap invalidation calls

Time Penalties

Issue a time penalty

penaltyManager.IssueTimePenalty(
    competitorId: 5,
    penaltySeconds: 5.0,
    reason: "Causing a collision",
    timestamp: Time.time
);

Cancel a time penalty

var penalties = penaltyManager.GetTimePenalties(competitorId);
if (penalties.Count > 0)
{
    penaltyManager.CancelTimePenalty(competitorId, penalties[^1].PenaltyId);
}

Get total penalty time

double totalPenalty = penaltyManager.GetTotalPenaltySeconds(competitorId);

Lap Invalidation

Invalidate a completed lap

lapInvalidationManager.InvalidateLap(
    session: session,
    competitorId: 5,
    lapNumber: 3,
    reason: "Track limits exceeded"
);

Invalidate the current in-progress lap

// The flag is applied when the lap completes
lapInvalidationManager.InvalidateCurrentLap(
    session: session,
    competitorId: 5,
    reason: "Off-track excursion"
);

Revalidate a lap

lapInvalidationManager.RevalidateLap(
    session: session,
    competitorId: 5,
    lapNumber: 3
);

Check if the current lap is flagged for invalidation

var competitors = RaceTimingManager.Instance.LapTimer.Competitors;
if (competitors.TryGetValue(competitorId, out var compData))
{
    if (compData.InvalidateCurrentLap)
    {
        Debug.Log($"Current lap will be invalid: {compData.CurrentLapInvalidationReason}");
    }
}

Get the best valid lap

var bestValidLap = lapInvalidationManager.GetBestValidLap(session, competitorId);
if (bestValidLap != null)
    Debug.Log($"Best valid lap: {bestValidLap.Duration:F3}s");
else
    Debug.Log("No valid laps yet");

Disqualification

Disqualify a competitor

penaltyManager.Disqualify(
    competitorId: 5,
    reason: "Dangerous driving",
    timestamp: Time.time
);

Check disqualification status

bool isDisqualified = penaltyManager.IsDisqualified(competitorId);
if (isDisqualified)
    Debug.Log($"Car {competitorId} is disqualified");

Note: IPenaltyManager does not expose a GetDisqualification() method. Use IsDisqualified(competitorId) to check status. The reason is only available through the OnDisqualification event at the time the penalty is issued.

Revoke a disqualification

penaltyManager.RevokeDisqualification(competitorId);

Events

All penalty operations fire events you can subscribe to:

Time penalty events

penaltyManager.OnTimePenaltyIssued += (e) =>
    Debug.Log($"Penalty: Car {e.CompetitorId} +{e.PenaltySeconds}s — {e.Reason}");

penaltyManager.OnTimePenaltyCancelled += (e) =>
    Debug.Log($"Penalty cancelled: Car {e.CompetitorId} -{e.PenaltySeconds}s");

Lap invalidation events

lapInvalidationManager.OnLapInvalidated += (e) =>
    Debug.Log($"Lap {e.LapNumber} invalidated: Car {e.CompetitorId} — {e.Reason}");

lapInvalidationManager.OnLapRevalidated += (e) =>
    Debug.Log($"Lap {e.LapNumber} revalidated: Car {e.CompetitorId}");

Disqualification events

penaltyManager.OnDisqualification += (e) =>
    Debug.Log($"Car {e.CompetitorId} DISQUALIFIED: {e.Reason}");

penaltyManager.OnDisqualificationRevoked += (e) =>
    Debug.Log($"Disqualification revoked: Car {e.CompetitorId}");

Inspector wiring (UnityEvents)

RaceTimingManager exposes all penalty events as UnityEvents for Inspector wiring:

RaceTimingManager.Instance.OnTimePenaltyIssuedUnity.AddListener(OnPenaltyIssued);
RaceTimingManager.Instance.OnLapInvalidatedUnity.AddListener(OnLapInvalidated);
RaceTimingManager.Instance.OnDisqualificationUnity.AddListener(OnDisqualification);

UI Integration

Race Tower

RaceTowerRowUI automatically shows: - Penalty time in yellow (e.g., +5.0s) - A DSQ badge in red for disqualified competitors

No additional setup required — the Race Tower queries the penalty manager automatically.

Toast Notifications

RaceToastBridge shows toasts for penalty events:

"LEC - +5.0s penalty - Causing a collision"
"VER - Lap 3 invalidated - Track limits"
"HAM - DISQUALIFIED - Dangerous driving"

Toggle penalty toasts individually in the RaceToastBridge Inspector: - Show Time Penalties - Show Lap Invalidations - Show Disqualifications

Demo Scene Keyboard Shortcuts

The demo scene includes keyboard shortcuts for testing penalties:

Key Action
P Issue 5s penalty to focused competitor
Shift+P Cancel last penalty for focused competitor
I Invalidate lap (current in progress or last completed)
Shift+I Revalidate lap (clear current lap flag or revalidate last)
D Disqualify focused competitor
Shift+D Revoke disqualification

Use N to cycle through competitors to focus different cars.

How Penalties Affect Rankings

Time Penalties

Time penalties are applied to the competitor's total race time when using PenaltyAwareStandardRaceRanking. This is not the default strategy — the default is StandardRaceRanking. To enable penalty-aware rankings:

var rd = RaceTimingManager.Instance.LapTimer.RaceDirector;
rd.SetRankingStrategy(new PenaltyAwareStandardRaceRanking(rd.PenaltyManager));

With this strategy active:

// Effective race time = actual time + penalties
effectiveTime = competitor.TotalRaceTime + penaltyManager.GetTotalPenaltySeconds(competitorId);

Disqualifications

Disqualified competitors: - Are excluded from standings entirely (they do not appear in the sorted list) - Are marked as "DSQ" in the UI - Are excluded from position calculations for other competitors

Lap Invalidations

Invalid laps: - Don't count for "best lap" times - Are filtered out in GapToBestLap tower mode - Don't affect sector best times - Are stored with CountsForBestLap = false and InvalidationReason

Best Practices

When to use each penalty type

Penalty When to use
Time Penalty Minor infractions, unsafe release, gaining an unfair advantage, post-race decisions
Lap Invalidation Going off-track, cutting corners, track limits violations in qualifying
Disqualification Major violations, dangerous driving, technical infringements, repeated offences

Current lap vs completed lap invalidation

  • Use InvalidateCurrentLap when the infraction happens during the lap (real-time detection)
  • Use InvalidateLap for post-lap review of completed laps

Preserving penalties across scene reloads

Penalties are tied to the Session object. On scene reload, either: - Serialize and restore the session state manually - Or keep AutoStartSession = false to prevent accidental session resets

Common Issues

Penalties not affecting standings

The default ranking strategy (StandardRaceRanking) does not account for penalties. To enable penalty-aware rankings, set the strategy explicitly:

var rd = RaceTimingManager.Instance.LapTimer.RaceDirector;
rd.SetRankingStrategy(new PenaltyAwareStandardRaceRanking(rd.PenaltyManager));

For qualifying-style sessions, use PenaltyAwareBestLapRanking instead:

rd.SetRankingStrategy(new PenaltyAwareBestLapRanking(rd.PenaltyManager, rd.LapInvalidationManager));

Invalidated laps still showing in Race Tower

The Race Tower uses GetBestValidLap() to filter invalid laps. If you wrote custom UI code, update it to query LapInvalidationManager.GetBestValidLap().

Can't revalidate a lap

Laps invalidated by the system for incomplete data (e.g., missing sector data) cannot be revalidated — this is by design.

Events not firing

Subscribe in Start() or later, not in Awake(). The RaceTimingManager must be initialized first.

API Reference Summary

IPenaltyManager Methods

  • IssueTimePenalty(competitorId, seconds, reason, timestamp) → int (penalty ID)
  • CancelTimePenalty(competitorId, penaltyId)
  • GetTimePenalties(competitorId) → IReadOnlyList\<TimePenalty>
  • GetTotalPenaltySeconds(competitorId) → double
  • Disqualify(competitorId, reason, timestamp)
  • IsDisqualified(competitorId) → bool
  • RevokeDisqualification(competitorId)
  • Reset()

ILapInvalidationManager Methods

  • InvalidateLap(session, competitorId, lapNumber, reason)
  • InvalidateCurrentLap(session, competitorId, reason)
  • RevalidateLap(session, competitorId, lapNumber)
  • LapCountsForBest(session, competitorId, lapNumber) → bool
  • GetBestValidLap(session, competitorId) → Lap?
  • GetAllValidLaps(session, competitorId) → IReadOnlyList\<Lap>
  • Reset()

Event Args

  • TimePenaltyIssuedEventArgs - CompetitorId, PenaltyId, PenaltySeconds, Reason, Timestamp
  • TimePenaltyCancelledEventArgs - CompetitorId, PenaltyId, PenaltySeconds
  • DisqualificationEventArgs - CompetitorId, Reason, Timestamp
  • DisqualificationRevokedEventArgs - CompetitorId
  • LapInvalidatedEventArgs - CompetitorId, LapNumber, Reason, IsAutoInvalidated
  • LapRevalidatedEventArgs - CompetitorId, LapNumber