Penalties System
Sirocco Race Timing includes a comprehensive penalties system for managing race infractions, time penalties, lap invalidations, and disqualifications.
Overview
The penalties system provides three main types of penalties:
- Time Penalties - Add seconds to a competitor's total race time (e.g., +5s, +10s)
- Lap Invalidations - Mark laps as invalid so they don't count for best lap times
- Disqualifications - Remove a competitor from the results entirely
Accessing the Penalty System
The penalty system is accessed through the RaceDirector:
using RaceTiming.Adapter;
using RaceTiming.Core;
// Get the penalty manager
var penaltyManager = RaceTimingManager.Instance.LapTimer.RaceDirector.PenaltyManager;
// Get the lap invalidation manager
var lapInvalidationManager = RaceTimingManager.Instance.LapTimer.RaceDirector.LapInvalidationManager;
// Get the session (needed for lap invalidation operations)
var session = RaceTimingManager.Instance.LapTimer.Session;
Time Penalties
Issuing a Time Penalty
// Issue a 5-second penalty
double currentTime = Time.time;
penaltyManager.IssueTimePenalty(
competitorId: 5,
penaltySeconds: 5.0,
reason: "Causing a collision",
timestamp: currentTime
);
Cancelling a Time Penalty
// Get all penalties for a competitor
var penalties = penaltyManager.GetTimePenalties(competitorId);
// Cancel a specific penalty by ID
if (penalties.Count > 0)
{
var lastPenalty = penalties[penalties.Count - 1];
penaltyManager.CancelTimePenalty(competitorId, lastPenalty.PenaltyId);
}
Getting Total Penalty Time
// Get the total penalty seconds for a competitor
double totalPenalty = penaltyManager.GetTotalPenaltySeconds(competitorId);
Console.WriteLine($"Total penalties: +{totalPenalty}s");
Lap Invalidation
Invalidating a Completed Lap
// Invalidate a specific lap number
lapInvalidationManager.InvalidateLap(
session: session,
competitorId: 5,
lapNumber: 3,
reason: "Track limits exceeded"
);
Invalidating the Current Lap In Progress
// Invalidate the lap currently being driven
// The lap will be marked as invalid when it's completed
lapInvalidationManager.InvalidateCurrentLap(
session: session,
competitorId: 5,
reason: "Off-track excursion"
);
Revalidating a Lap
// Revalidate a previously invalidated lap
lapInvalidationManager.RevalidateLap(
session: session,
competitorId: 5,
lapNumber: 3
);
Checking If Current Lap Is Marked 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}");
// Optionally clear the flag
compData.InvalidateCurrentLap = false;
compData.CurrentLapInvalidationReason = null;
}
}
Getting Best Valid Lap
// Get the best lap that counts for best lap times (excludes invalid laps)
var bestValidLap = lapInvalidationManager.GetBestValidLap(session, competitorId);
if (bestValidLap != null)
{
Debug.Log($"Best valid lap: {bestValidLap.Duration:F3}s");
}
else
{
Debug.Log("No valid laps");
}
Disqualification
Disqualifying a Competitor
// Disqualify a competitor
double currentTime = Time.time;
penaltyManager.Disqualify(
competitorId: 5,
reason: "Dangerous driving",
timestamp: currentTime
);
Checking Disqualification Status
bool isDisqualified = penaltyManager.IsDisqualified(competitorId);
if (isDisqualified)
{
var dsq = penaltyManager.GetDisqualification(competitorId);
Debug.Log($"DSQ Reason: {dsq.Reason}");
}
Revoking a Disqualification
// Reinstate a disqualified competitor
penaltyManager.RevokeDisqualification(competitorId);
Events
All penalty operations fire events that you can subscribe to:
Time Penalty Events
// Subscribe to penalty events
penaltyManager.OnTimePenaltyIssued += (e) =>
{
Debug.Log($"Penalty issued to {e.CompetitorId}: +{e.PenaltySeconds}s - {e.Reason}");
};
penaltyManager.OnTimePenaltyCancelled += (e) =>
{
Debug.Log($"Penalty cancelled for {e.CompetitorId}: -{e.PenaltySeconds}s");
};
Lap Invalidation Events
lapInvalidationManager.OnLapInvalidated += (e) =>
{
Debug.Log($"Lap {e.LapNumber} invalidated for {e.CompetitorId}: {e.Reason}");
};
lapInvalidationManager.OnLapRevalidated += (e) =>
{
Debug.Log($"Lap {e.LapNumber} revalidated for {e.CompetitorId}");
};
Disqualification Events
penaltyManager.OnDisqualification += (e) =>
{
Debug.Log($"Competitor {e.CompetitorId} DISQUALIFIED: {e.Reason}");
};
penaltyManager.OnDisqualificationRevoked += (e) =>
{
Debug.Log($"Disqualification revoked for {e.CompetitorId}");
};
Unity Events (For Inspector Wiring)
The RaceTimingManager component provides UnityEvent wrappers for all penalty events:
// Access Unity events
RaceTimingManager.Instance.OnTimePenaltyIssuedUnity.AddListener(OnPenaltyIssued);
RaceTimingManager.Instance.OnLapInvalidatedUnity.AddListener(OnLapInvalidated);
RaceTimingManager.Instance.OnDisqualificationUnity.AddListener(OnDisqualification);
These can also be wired up in the Inspector on the RaceTimingManager GameObject.
UI Integration
Race Tower Display
The RaceTowerRowUI component automatically displays:
- Penalty seconds in yellow (e.g., "+5.0s")
- DSQ badge in red for disqualified competitors
- Penalties are hidden when no penalties exist
No additional setup required - the Race Tower queries the penalty manager automatically.
Toast Notifications
The RaceToastBridge component subscribes to penalty events and shows toast notifications:
"LEC - +5.0s penalty - Causing a collision"
"VER - Lap 3 invalidated - Track limits"
"HAM - DISQUALIFIED - Dangerous driving"
Enable/disable specific penalty toasts in the 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 the PenaltyAwareRankingStrategy (default):
// Effective race time = actual time + penalties
effectiveTime = competitor.TotalRaceTime + penaltyManager.GetTotalPenaltySeconds(competitorId);
Disqualifications
Disqualified competitors: - Appear at the bottom of the standings - 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
Time Penalties: - Minor infractions (track limits, unsafe release) - Gaining an unfair advantage - Post-race penalties
Lap Invalidations: - Going off-track during a lap - Cutting corners - Track limits violations during qualifying
Disqualifications: - Major rule violations - Dangerous driving - Technical infringements - Multiple repeated violations
Current Lap vs Completed Lap Invalidation
Use current lap invalidation when: - You detect an infringement in real-time (during the lap) - You want to warn the driver before they complete the lap - The lap hasn't been completed yet
Use completed lap invalidation when: - Reviewing laps after they're finished - Post-lap analysis - Overriding auto-validated laps
Preventing Penalties Reset
Penalties are stored in the Session object. To preserve penalties across scene reloads:
1. Serialize the session data before reload
2. Restore penalty state after reload
3. Or prevent session reset using AutoStartSession = false
Common Issues
"Penalties not affecting standings"
Solution: Ensure you're using PenaltyAwareRankingStrategy (it's the default). Check:
var raceDirector = RaceTimingManager.Instance.LapTimer.RaceDirector;
Debug.Log($"Ranking strategy: {raceDirector.RankingStrategy.GetType().Name}");
"Invalidated laps still showing in Race Tower"
Solution: Ensure you're using the latest version. The Race Tower now uses GetBestValidLap() to filter invalid laps. If using custom UI code, update to query the lap invalidation manager.
"Can't revalidate auto-invalidated laps"
Expected behavior: Laps that are auto-invalidated (e.g., incomplete sector data) cannot be revalidated. This is by design to maintain data integrity.
"Events not firing"
Solution: Ensure events are subscribed after the RaceTimingManager is initialized. Use Start() or later, not Awake().
API Reference Summary
IPenaltyManager Methods
IssueTimePenalty(competitorId, seconds, reason, timestamp)CancelTimePenalty(competitorId, penaltyId)GetTimePenalties(competitorId)→ ListGetTotalPenaltySeconds(competitorId)→ doubleDisqualify(competitorId, reason, timestamp)IsDisqualified(competitorId)→ boolGetDisqualification(competitorId)→ DisqualificationRevokeDisqualification(competitorId)
ILapInvalidationManager Methods
InvalidateLap(session, competitorId, lapNumber, reason)InvalidateCurrentLap(session, competitorId, reason)RevalidateLap(session, competitorId, lapNumber)IsLapInvalid(session, competitorId, lapNumber)→ boolGetBestValidLap(session, competitorId)→ Lap?GetInvalidationReason(session, competitorId, lapNumber)→ string
Event Args
TimePenaltyIssuedEventArgs- CompetitorId, PenaltyId, PenaltySeconds, Reason, TimestampTimePenaltyCancelledEventArgs- CompetitorId, PenaltyId, PenaltySecondsDisqualificationEventArgs- CompetitorId, Reason, TimestampDisqualificationRevokedEventArgs- CompetitorIdLapInvalidatedEventArgs- CompetitorId, LapNumber, ReasonLapRevalidatedEventArgs- CompetitorId, LapNumber