From 251941393e7efa1020a028d4c296b54f184317de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kaiser?= Date: Wed, 11 Feb 2026 12:38:38 +0100 Subject: [PATCH 1/2] F1 replay file sources --- .../Formula1/F12022RaceReplaySource.cs | 41 +++++++++ .../Formula1/F12023RaceReplaySource.cs | 41 +++++++++ .../Formula1/F12024RaceReplaySource.cs | 41 +++++++++ .../Formula1/F12025RaceReplaySource.cs | 41 +++++++++ .../Sources/Formula1/F1RaceReplaySource.cs | 91 +++++++++++++++++++ .../Formula1/F1RaceReplaySourceBase.cs | 63 +++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs create mode 100644 GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs create mode 100644 GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs create mode 100644 GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs create mode 100644 GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs create mode 100644 GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs diff --git a/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs new file mode 100644 index 0000000..8be9015 --- /dev/null +++ b/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs @@ -0,0 +1,41 @@ +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(EnsurePath(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)) + { + options.Path = GetDefaultReplayPath(); + } + 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..fc1bf97 --- /dev/null +++ b/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs @@ -0,0 +1,41 @@ +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(EnsurePath(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)) + { + options.Path = GetDefaultReplayPath(); + } + 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..4e8b4e9 --- /dev/null +++ b/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs @@ -0,0 +1,41 @@ +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(EnsurePath(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)) + { + options.Path = GetDefaultReplayPath(); + } + 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..fa8014d --- /dev/null +++ b/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs @@ -0,0 +1,41 @@ +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(EnsurePath(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)) + { + options.Path = GetDefaultReplayPath(); + } + 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..60bdf68 --- /dev/null +++ b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; + +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(EnsurePath(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)) + { + options.Path = F12025RaceReplaySource.GetDefaultReplayPath(); + } + 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" }; + foreach (var year in years) + { + var path = GetReplayPathForYear(year); + if (Directory.Exists(path)) + { + return path; + } + } + + // Default to F1 25 even if it doesn't exist + return 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..e73c7dc --- /dev/null +++ b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs @@ -0,0 +1,63 @@ +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 + { + 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 = TimeSpan.FromSeconds(2) + }; + } + + /// + /// 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, + DebounceDelay = options.DebounceDelay == default + ? TimeSpan.FromSeconds(2) + : options.DebounceDelay + }; + } + } +} From 40532a27788d31eff045d40dec4601884aebf893 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:09:29 +0100 Subject: [PATCH 2/2] Fix immutability violations and redundant initialization in F1 replay sources (#13) * Initial plan * Fix EnsurePath to return new instances and update foreach to Select Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Extract debounce delay constants for better maintainability Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Add clarifying comments and improve variable naming Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> * Document library default coupling in constant Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: codegefluester <203914+codegefluester@users.noreply.github.com> --- .../Formula1/F12022RaceReplaySource.cs | 10 ++++++-- .../Formula1/F12023RaceReplaySource.cs | 10 ++++++-- .../Formula1/F12024RaceReplaySource.cs | 10 ++++++-- .../Formula1/F12025RaceReplaySource.cs | 10 ++++++-- .../Sources/Formula1/F1RaceReplaySource.cs | 24 ++++++++++--------- .../Formula1/F1RaceReplaySourceBase.cs | 20 +++++++++++++--- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs index 8be9015..652116a 100644 --- a/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs +++ b/GamesDat/Telemetry/Sources/Formula1/F12022RaceReplaySource.cs @@ -7,7 +7,7 @@ namespace GamesDat.Core.Telemetry.Sources.Formula1 public class F12022RaceReplaySource : F1RaceReplaySourceBase { public F12022RaceReplaySource(FileWatcherOptions options) - : base(ApplyDefaults(EnsurePath(options))) + : base(ApplyDefaults(options)) { } @@ -25,7 +25,13 @@ private static FileWatcherOptions EnsurePath(FileWatcherOptions options) { if (string.IsNullOrEmpty(options.Path)) { - options.Path = GetDefaultReplayPath(); + return new FileWatcherOptions + { + Path = GetDefaultReplayPath(), + Patterns = options.Patterns, + IncludeSubdirectories = options.IncludeSubdirectories, + DebounceDelay = options.DebounceDelay + }; } return options; } diff --git a/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs index fc1bf97..d46325e 100644 --- a/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs +++ b/GamesDat/Telemetry/Sources/Formula1/F12023RaceReplaySource.cs @@ -7,7 +7,7 @@ namespace GamesDat.Core.Telemetry.Sources.Formula1 public class F12023RaceReplaySource : F1RaceReplaySourceBase { public F12023RaceReplaySource(FileWatcherOptions options) - : base(ApplyDefaults(EnsurePath(options))) + : base(ApplyDefaults(options)) { } @@ -25,7 +25,13 @@ private static FileWatcherOptions EnsurePath(FileWatcherOptions options) { if (string.IsNullOrEmpty(options.Path)) { - options.Path = GetDefaultReplayPath(); + return new FileWatcherOptions + { + Path = GetDefaultReplayPath(), + Patterns = options.Patterns, + IncludeSubdirectories = options.IncludeSubdirectories, + DebounceDelay = options.DebounceDelay + }; } return options; } diff --git a/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs index 4e8b4e9..a685f40 100644 --- a/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs +++ b/GamesDat/Telemetry/Sources/Formula1/F12024RaceReplaySource.cs @@ -7,7 +7,7 @@ namespace GamesDat.Core.Telemetry.Sources.Formula1 public class F12024RaceReplaySource : F1RaceReplaySourceBase { public F12024RaceReplaySource(FileWatcherOptions options) - : base(ApplyDefaults(EnsurePath(options))) + : base(ApplyDefaults(options)) { } @@ -25,7 +25,13 @@ private static FileWatcherOptions EnsurePath(FileWatcherOptions options) { if (string.IsNullOrEmpty(options.Path)) { - options.Path = GetDefaultReplayPath(); + return new FileWatcherOptions + { + Path = GetDefaultReplayPath(), + Patterns = options.Patterns, + IncludeSubdirectories = options.IncludeSubdirectories, + DebounceDelay = options.DebounceDelay + }; } return options; } diff --git a/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs index fa8014d..905e589 100644 --- a/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs +++ b/GamesDat/Telemetry/Sources/Formula1/F12025RaceReplaySource.cs @@ -7,7 +7,7 @@ namespace GamesDat.Core.Telemetry.Sources.Formula1 public class F12025RaceReplaySource : F1RaceReplaySourceBase { public F12025RaceReplaySource(FileWatcherOptions options) - : base(ApplyDefaults(EnsurePath(options))) + : base(ApplyDefaults(options)) { } @@ -25,7 +25,13 @@ private static FileWatcherOptions EnsurePath(FileWatcherOptions options) { if (string.IsNullOrEmpty(options.Path)) { - options.Path = GetDefaultReplayPath(); + return new FileWatcherOptions + { + Path = GetDefaultReplayPath(), + Patterns = options.Patterns, + IncludeSubdirectories = options.IncludeSubdirectories, + DebounceDelay = options.DebounceDelay + }; } return options; } diff --git a/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs index 60bdf68..97ffaec 100644 --- a/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs +++ b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySource.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; namespace GamesDat.Core.Telemetry.Sources.Formula1 { @@ -12,7 +13,7 @@ namespace GamesDat.Core.Telemetry.Sources.Formula1 public class F1RaceReplaySource : F1RaceReplaySourceBase { public F1RaceReplaySource(FileWatcherOptions options) - : base(ApplyDefaults(EnsurePath(options))) + : base(ApplyDefaults(options)) { } @@ -23,7 +24,13 @@ private static FileWatcherOptions EnsurePath(FileWatcherOptions options) { if (string.IsNullOrEmpty(options.Path)) { - options.Path = F12025RaceReplaySource.GetDefaultReplayPath(); + return new FileWatcherOptions + { + Path = F12025RaceReplaySource.GetDefaultReplayPath(), + Patterns = options.Patterns, + IncludeSubdirectories = options.IncludeSubdirectories, + DebounceDelay = options.DebounceDelay + }; } return options; } @@ -60,17 +67,12 @@ public F1RaceReplaySource(string? customPath = null, bool autoDetectLatestInstal public static string GetLatestInstalledReplayPath() { var years = new[] { "25", "24", "23", "22" }; - foreach (var year in years) - { - var path = GetReplayPathForYear(year); - if (Directory.Exists(path)) - { - return path; - } - } + var existingPath = years + .Select(GetReplayPathForYear) + .FirstOrDefault(Directory.Exists); // Default to F1 25 even if it doesn't exist - return GetReplayPathForYear("25"); + return existingPath ?? GetReplayPathForYear("25"); } private static string ResolveReplayPath(string? customPath, bool autoDetectLatestInstalled) diff --git a/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs index e73c7dc..f459d5f 100644 --- a/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs +++ b/GamesDat/Telemetry/Sources/Formula1/F1RaceReplaySourceBase.cs @@ -8,6 +8,18 @@ namespace GamesDat.Core.Telemetry.Sources.Formula1 /// 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) { } @@ -33,7 +45,7 @@ protected static FileWatcherOptions CreateDefaultOptions(string path) Path = path, Patterns = new[] { "*.frr" }, IncludeSubdirectories = false, - DebounceDelay = TimeSpan.FromSeconds(2) + DebounceDelay = F1DebounceDelay }; } @@ -54,8 +66,10 @@ protected static FileWatcherOptions ApplyDefaults(FileWatcherOptions options) ? new[] { "*.frr" } : options.Patterns, IncludeSubdirectories = options.IncludeSubdirectories, - DebounceDelay = options.DebounceDelay == default - ? TimeSpan.FromSeconds(2) + // 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 }; }