Skip to content

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:

  1. Time Penalties - Add seconds to a competitor's total race time (e.g., +5s, +10s)
  2. Lap Invalidations - Mark laps as invalid so they don't count for best lap times
  3. 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) → List
  • GetTotalPenaltySeconds(competitorId) → double
  • Disqualify(competitorId, reason, timestamp)
  • IsDisqualified(competitorId) → bool
  • GetDisqualification(competitorId) → Disqualification
  • RevokeDisqualification(competitorId)

ILapInvalidationManager Methods

  • InvalidateLap(session, competitorId, lapNumber, reason)
  • InvalidateCurrentLap(session, competitorId, reason)
  • RevalidateLap(session, competitorId, lapNumber)
  • IsLapInvalid(session, competitorId, lapNumber) → bool
  • GetBestValidLap(session, competitorId) → Lap?
  • GetInvalidationReason(session, competitorId, lapNumber) → string

Event Args

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