diff --git a/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs
new file mode 100644
index 0000000..652116a
--- /dev/null
+++ b/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs
@@ -0,0 +1,47 @@
+namespace GamesDat.Core.Telemetry.Sources.Formula1
+{
+ ///
+ /// File watcher source for F1 2022 race replay files.
+ /// Monitors: Documents\My Games\F1 22\replays
+ ///
+ public class F12022RaceReplaySource : F1RaceReplaySourceBase
+ {
+ public F12022RaceReplaySource(FileWatcherOptions options)
+ : base(ApplyDefaults(options))
+ {
+ }
+
+ public F12022RaceReplaySource(string? customPath = null)
+ : base(CreateDefaultOptions(customPath ?? GetDefaultReplayPath()))
+ {
+ }
+
+ public static string GetDefaultReplayPath() => GetReplayPathForYear("22");
+
+ ///
+ /// Ensure options has a path set, defaulting to F1 22 if not provided
+ ///
+ private static FileWatcherOptions EnsurePath(FileWatcherOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Path))
+ {
+ return new FileWatcherOptions
+ {
+ Path = GetDefaultReplayPath(),
+ Patterns = options.Patterns,
+ IncludeSubdirectories = options.IncludeSubdirectories,
+ DebounceDelay = options.DebounceDelay
+ };
+ }
+ return options;
+ }
+
+ ///
+ /// Apply F1 22-specific defaults. Used by test discovery.
+ ///
+ private static new FileWatcherOptions ApplyDefaults(FileWatcherOptions options)
+ {
+ return F1RaceReplaySourceBase.ApplyDefaults(EnsurePath(options));
+ }
+ }
+}
diff --git a/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs
new file mode 100644
index 0000000..d46325e
--- /dev/null
+++ b/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs
@@ -0,0 +1,47 @@
+namespace GamesDat.Core.Telemetry.Sources.Formula1
+{
+ ///
+ /// File watcher source for F1 2023 race replay files.
+ /// Monitors: Documents\My Games\F1 23\replays
+ ///
+ public class F12023RaceReplaySource : F1RaceReplaySourceBase
+ {
+ public F12023RaceReplaySource(FileWatcherOptions options)
+ : base(ApplyDefaults(options))
+ {
+ }
+
+ public F12023RaceReplaySource(string? customPath = null)
+ : base(CreateDefaultOptions(customPath ?? GetDefaultReplayPath()))
+ {
+ }
+
+ public static string GetDefaultReplayPath() => GetReplayPathForYear("23");
+
+ ///
+ /// Ensure options has a path set, defaulting to F1 23 if not provided
+ ///
+ private static FileWatcherOptions EnsurePath(FileWatcherOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Path))
+ {
+ return new FileWatcherOptions
+ {
+ Path = GetDefaultReplayPath(),
+ Patterns = options.Patterns,
+ IncludeSubdirectories = options.IncludeSubdirectories,
+ DebounceDelay = options.DebounceDelay
+ };
+ }
+ return options;
+ }
+
+ ///
+ /// Apply F1 23-specific defaults. Used by test discovery.
+ ///
+ private static new FileWatcherOptions ApplyDefaults(FileWatcherOptions options)
+ {
+ return F1RaceReplaySourceBase.ApplyDefaults(EnsurePath(options));
+ }
+ }
+}
diff --git a/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs
new file mode 100644
index 0000000..a685f40
--- /dev/null
+++ b/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs
@@ -0,0 +1,47 @@
+namespace GamesDat.Core.Telemetry.Sources.Formula1
+{
+ ///
+ /// File watcher source for F1 2024 race replay files.
+ /// Monitors: Documents\My Games\F1 24\replays
+ ///
+ public class F12024RaceReplaySource : F1RaceReplaySourceBase
+ {
+ public F12024RaceReplaySource(FileWatcherOptions options)
+ : base(ApplyDefaults(options))
+ {
+ }
+
+ public F12024RaceReplaySource(string? customPath = null)
+ : base(CreateDefaultOptions(customPath ?? GetDefaultReplayPath()))
+ {
+ }
+
+ public static string GetDefaultReplayPath() => GetReplayPathForYear("24");
+
+ ///
+ /// Ensure options has a path set, defaulting to F1 24 if not provided
+ ///
+ private static FileWatcherOptions EnsurePath(FileWatcherOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Path))
+ {
+ return new FileWatcherOptions
+ {
+ Path = GetDefaultReplayPath(),
+ Patterns = options.Patterns,
+ IncludeSubdirectories = options.IncludeSubdirectories,
+ DebounceDelay = options.DebounceDelay
+ };
+ }
+ return options;
+ }
+
+ ///
+ /// Apply F1 24-specific defaults. Used by test discovery.
+ ///
+ private static new FileWatcherOptions ApplyDefaults(FileWatcherOptions options)
+ {
+ return F1RaceReplaySourceBase.ApplyDefaults(EnsurePath(options));
+ }
+ }
+}
diff --git a/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs
new file mode 100644
index 0000000..905e589
--- /dev/null
+++ b/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs
@@ -0,0 +1,47 @@
+namespace GamesDat.Core.Telemetry.Sources.Formula1
+{
+ ///
+ /// File watcher source for F1 2025 race replay files.
+ /// Monitors: Documents\My Games\F1 25\replays
+ ///
+ public class F12025RaceReplaySource : F1RaceReplaySourceBase
+ {
+ public F12025RaceReplaySource(FileWatcherOptions options)
+ : base(ApplyDefaults(options))
+ {
+ }
+
+ public F12025RaceReplaySource(string? customPath = null)
+ : base(CreateDefaultOptions(customPath ?? GetDefaultReplayPath()))
+ {
+ }
+
+ public static string GetDefaultReplayPath() => GetReplayPathForYear("25");
+
+ ///
+ /// Ensure options has a path set, defaulting to F1 25 if not provided
+ ///
+ private static FileWatcherOptions EnsurePath(FileWatcherOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Path))
+ {
+ return new FileWatcherOptions
+ {
+ Path = GetDefaultReplayPath(),
+ Patterns = options.Patterns,
+ IncludeSubdirectories = options.IncludeSubdirectories,
+ DebounceDelay = options.DebounceDelay
+ };
+ }
+ return options;
+ }
+
+ ///
+ /// Apply F1 25-specific defaults. Used by test discovery.
+ ///
+ private static new FileWatcherOptions ApplyDefaults(FileWatcherOptions options)
+ {
+ return F1RaceReplaySourceBase.ApplyDefaults(EnsurePath(options));
+ }
+ }
+}
diff --git a/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs
new file mode 100644
index 0000000..97ffaec
--- /dev/null
+++ b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs
@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace GamesDat.Core.Telemetry.Sources.Formula1
+{
+ ///
+ /// File watcher source for F1 race replay files.
+ /// Defaults to F1 25, but can auto-detect the latest installed F1 game.
+ /// For monitoring specific years or multiple years simultaneously, use year-specific sources:
+ /// F12025RaceReplaySource, F12024RaceReplaySource, F12023RaceReplaySource, F12022RaceReplaySource
+ ///
+ public class F1RaceReplaySource : F1RaceReplaySourceBase
+ {
+ public F1RaceReplaySource(FileWatcherOptions options)
+ : base(ApplyDefaults(options))
+ {
+ }
+
+ ///
+ /// Ensure options has a path set, defaulting to F1 25 if not provided
+ ///
+ private static FileWatcherOptions EnsurePath(FileWatcherOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Path))
+ {
+ return new FileWatcherOptions
+ {
+ Path = F12025RaceReplaySource.GetDefaultReplayPath(),
+ Patterns = options.Patterns,
+ IncludeSubdirectories = options.IncludeSubdirectories,
+ DebounceDelay = options.DebounceDelay
+ };
+ }
+ return options;
+ }
+
+ ///
+ /// Apply F1 defaults. Used by test discovery.
+ ///
+ private static new FileWatcherOptions ApplyDefaults(FileWatcherOptions options)
+ {
+ return F1RaceReplaySourceBase.ApplyDefaults(EnsurePath(options));
+ }
+
+ ///
+ /// Create F1 replay source with optional custom path and detection strategy
+ ///
+ /// Custom replay folder path. If provided, autoDetectLatestInstalled is ignored.
+ /// If true and customPath is null, auto-detects the latest installed F1 game (checks 25, 24, 23, 22). If false, defaults to F1 25.
+ public F1RaceReplaySource(string? customPath = null, bool autoDetectLatestInstalled = false)
+ : base(CreateDefaultOptions(ResolveReplayPath(customPath, autoDetectLatestInstalled)))
+ {
+ }
+
+ ///
+ /// Get the default F1 replay folder path (F1 25)
+ ///
+ public static string GetDefaultReplayPath() => F12025RaceReplaySource.GetDefaultReplayPath();
+
+ ///
+ /// Auto-detect the latest installed F1 game's replay folder.
+ /// Checks F1 25, F1 24, F1 23, F1 22 in order and returns the first that exists.
+ /// Note: This checks for folder existence, which may include cases where the game
+ /// is uninstalled but replay folders remain.
+ ///
+ public static string GetLatestInstalledReplayPath()
+ {
+ var years = new[] { "25", "24", "23", "22" };
+ var existingPath = years
+ .Select(GetReplayPathForYear)
+ .FirstOrDefault(Directory.Exists);
+
+ // Default to F1 25 even if it doesn't exist
+ return existingPath ?? GetReplayPathForYear("25");
+ }
+
+ private static string ResolveReplayPath(string? customPath, bool autoDetectLatestInstalled)
+ {
+ if (customPath != null)
+ {
+ return customPath;
+ }
+
+ if (autoDetectLatestInstalled)
+ {
+ return GetLatestInstalledReplayPath();
+ }
+
+ return GetDefaultReplayPath();
+ }
+ }
+}
diff --git a/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs
new file mode 100644
index 0000000..f459d5f
--- /dev/null
+++ b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs
@@ -0,0 +1,77 @@
+using System;
+
+namespace GamesDat.Core.Telemetry.Sources.Formula1
+{
+ ///
+ /// Abstract base class for F1 race replay file sources across different game years.
+ /// Provides common functionality for monitoring F1 replay directories.
+ ///
+ public abstract class F1RaceReplaySourceBase : FileWatcherSourceBase
+ {
+ ///
+ /// The default debounce delay used by FileWatcherOptions (1 second).
+ /// Note: This mirrors the default in FileWatcherOptions.DebounceDelay.
+ /// If that default changes, this constant should be updated to match.
+ ///
+ private static readonly TimeSpan LibraryDefaultDebounceDelay = TimeSpan.FromSeconds(1);
+
+ ///
+ /// The F1-specific debounce delay (2 seconds)
+ ///
+ private static readonly TimeSpan F1DebounceDelay = TimeSpan.FromSeconds(2);
+
+ protected F1RaceReplaySourceBase(FileWatcherOptions options) : base(options)
+ {
+ }
+
+ ///
+ /// Build the full replay path for a specific F1 game year
+ ///
+ protected static string GetReplayPathForYear(string gameYear)
+ {
+ var documentsFolder = Environment.GetFolderPath(
+ Environment.SpecialFolder.MyDocuments,
+ Environment.SpecialFolderOption.DoNotVerify);
+ return System.IO.Path.Combine(documentsFolder, "My Games", $"F1 {gameYear}", "replays");
+ }
+
+ ///
+ /// Create default FileWatcherOptions for F1 replay monitoring
+ ///
+ protected static FileWatcherOptions CreateDefaultOptions(string path)
+ {
+ return new FileWatcherOptions
+ {
+ Path = path,
+ Patterns = new[] { "*.frr" },
+ IncludeSubdirectories = false,
+ DebounceDelay = F1DebounceDelay
+ };
+ }
+
+ ///
+ /// Apply F1-specific defaults to options. Requires Path to be non-empty.
+ ///
+ protected static FileWatcherOptions ApplyDefaults(FileWatcherOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Path))
+ {
+ throw new ArgumentException("FileWatcherOptions.Path cannot be null or empty. Provide a path before calling ApplyDefaults.", nameof(options));
+ }
+
+ return new FileWatcherOptions
+ {
+ Path = options.Path,
+ Patterns = options.Patterns == null || options.Patterns.Length == 0
+ ? new[] { "*.frr" }
+ : options.Patterns,
+ IncludeSubdirectories = options.IncludeSubdirectories,
+ // Check for both default (00:00:00, used when constructed via string path)
+ // and LibraryDefaultDebounceDelay (1s, used when constructed via options without explicit delay)
+ DebounceDelay = options.DebounceDelay == default || options.DebounceDelay == LibraryDefaultDebounceDelay
+ ? F1DebounceDelay
+ : options.DebounceDelay
+ };
+ }
+ }
+}