Penalties System
Sirocco Race Timing supports three types of penalties:
- Time Penalties — add seconds to a competitor's race time (e.g., +5s)
- Lap Invalidations — mark laps as invalid so they don't count for best-lap purposes
- 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:
IPenaltyManagerdoes not expose aGetDisqualification()method. UseIsDisqualified(competitorId)to check status. The reason is only available through theOnDisqualificationevent 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
InvalidateCurrentLapwhen the infraction happens during the lap (real-time detection) - Use
InvalidateLapfor 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)→ doubleDisqualify(competitorId, reason, timestamp)IsDisqualified(competitorId)→ boolRevokeDisqualification(competitorId)Reset()
ILapInvalidationManager Methods
InvalidateLap(session, competitorId, lapNumber, reason)InvalidateCurrentLap(session, competitorId, reason)RevalidateLap(session, competitorId, lapNumber)LapCountsForBest(session, competitorId, lapNumber)→ boolGetBestValidLap(session, competitorId)→ Lap?GetAllValidLaps(session, competitorId)→ IReadOnlyList\<Lap>Reset()
Event Args
TimePenaltyIssuedEventArgs- CompetitorId, PenaltyId, PenaltySeconds, Reason, TimestampTimePenaltyCancelledEventArgs- CompetitorId, PenaltyId, PenaltySecondsDisqualificationEventArgs- CompetitorId, Reason, TimestampDisqualificationRevokedEventArgs- CompetitorIdLapInvalidatedEventArgs- CompetitorId, LapNumber, Reason, IsAutoInvalidatedLapRevalidatedEventArgs- CompetitorId, LapNumber