diff --git a/dagger.json b/dagger.json index a24defb..cb24932 100644 --- a/dagger.json +++ b/dagger.json @@ -12,5 +12,5 @@ } ], "source": "dagger", - "engineVersion": "v0.13.3" + "engineVersion": "v0.13.5" } diff --git a/src/Dota2Helper.Desktop/Dota2Helper.Desktop.csproj b/src/Dota2Helper.Desktop/Dota2Helper.Desktop.csproj index afcdc8a..dbfdb35 100644 --- a/src/Dota2Helper.Desktop/Dota2Helper.Desktop.csproj +++ b/src/Dota2Helper.Desktop/Dota2Helper.Desktop.csproj @@ -33,5 +33,6 @@ Always + diff --git a/src/Dota2Helper.Desktop/gamestate_integration_timers.cfg b/src/Dota2Helper.Desktop/gamestate_integration_timers.cfg new file mode 100644 index 0000000..31ec37a --- /dev/null +++ b/src/Dota2Helper.Desktop/gamestate_integration_timers.cfg @@ -0,0 +1,17 @@ +"Dota 2 Integration Configuration" +{ + "uri" "http://localhost:4001/" + "timeout" "5.0" + "buffer" "0.1" + "throttle" "0.1" + "heartbeat" "1.0" + "data" + { + "provider" "0" + "map" "1" + "player" "0" + "hero" "0" + "abilities" "0" + "items" "0" + } +} \ No newline at end of file diff --git a/src/Dota2Helper/App.axaml.cs b/src/Dota2Helper/App.axaml.cs index d16af1d..8657e94 100644 --- a/src/Dota2Helper/App.axaml.cs +++ b/src/Dota2Helper/App.axaml.cs @@ -68,6 +68,7 @@ private static IHost CreateHost() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Logging.ClearProviders(); builder.Logging.AddDebug(); diff --git a/src/Dota2Helper/Core/Gsi/GameState.cs b/src/Dota2Helper/Core/Gsi/GameState.cs index 44044ab..1b8a23d 100644 --- a/src/Dota2Helper/Core/Gsi/GameState.cs +++ b/src/Dota2Helper/Core/Gsi/GameState.cs @@ -7,11 +7,11 @@ public class GameState { [JsonIgnore] // ReSharper disable once InconsistentNaming - private const string DOTA_GAMERULES_STATE_PRE_GAME = nameof(DOTA_GAMERULES_STATE_PRE_GAME); + const string DOTA_GAMERULES_STATE_PRE_GAME = nameof(DOTA_GAMERULES_STATE_PRE_GAME); [JsonIgnore] // ReSharper disable once InconsistentNaming - private const string DOTA_GAMERULES_STATE_GAME_IN_PROGRESS = nameof(DOTA_GAMERULES_STATE_GAME_IN_PROGRESS); + const string DOTA_GAMERULES_STATE_GAME_IN_PROGRESS = nameof(DOTA_GAMERULES_STATE_GAME_IN_PROGRESS); [JsonPropertyName("map")] public Map? Map { get; set; } diff --git a/src/Dota2Helper/Core/Gsi/GameStateHolder.cs b/src/Dota2Helper/Core/Gsi/GameStateHolder.cs index af37f9f..0c3b2bc 100644 --- a/src/Dota2Helper/Core/Gsi/GameStateHolder.cs +++ b/src/Dota2Helper/Core/Gsi/GameStateHolder.cs @@ -4,10 +4,9 @@ namespace Dota2Helper.Core.Gsi; public class GameStateHolder { - - private static readonly object Lock = new(); - - private GameState? _state; + static readonly object Lock = new(); + + GameState? _state; public GameState? State { diff --git a/src/Dota2Helper/Core/Gsi/SteamLibraryService.cs b/src/Dota2Helper/Core/Gsi/SteamLibraryService.cs new file mode 100644 index 0000000..0b9c5db --- /dev/null +++ b/src/Dota2Helper/Core/Gsi/SteamLibraryService.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; + +namespace Dota2Helper.Core.Gsi; + +public partial class SteamLibraryService(ILogger logger) +{ + const string ConfigFile = "gamestate_integration_timers.cfg"; + + string? FindGameStateIntegrationPath() + { + var dota2FolderPath = FindDota2InstallationPath(); + + if (dota2FolderPath == null) return null; + + var gameStateIntegrationPath = Path.Combine(dota2FolderPath, "game", "dota", "cfg", "gamestate_integration"); + return Directory.Exists(gameStateIntegrationPath) ? gameStateIntegrationPath : null; + } + + public bool IsIntegrationInstalled() + { + var gameStateIntegrationPath = FindGameStateIntegrationPath(); + + if (gameStateIntegrationPath == null) return false; + + var configFileFullPath = Path.Combine(gameStateIntegrationPath, ConfigFile); + return File.Exists(configFileFullPath); + } + + string? FindDota2InstallationPath() + { + var libraryFoldersPath = GetLibraryFoldersPath(); + + if (string.IsNullOrEmpty(libraryFoldersPath) || !File.Exists(libraryFoldersPath)) + { + logger.LogError("libraryfolders.vdf not found"); + return null; + } + + var steamInstallPath = GetSteamInstallationPath(); + + if (string.IsNullOrEmpty(steamInstallPath)) + { + logger.LogError("Steam installation path not found"); + return null; + } + + var steamLibraryPaths = GetSteamLibraryPaths(libraryFoldersPath); + + foreach (var steamLibraryPath in steamLibraryPaths) + { + var dota2FolderPath = Path.Combine(steamLibraryPath, "steamapps", "common", "dota 2 beta"); + + if (Directory.Exists(dota2FolderPath)) return dota2FolderPath; + } + + return null; + } + + string? GetSteamInstallationPath() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? GetWindowsInstall() : + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? GetMacInstall() : null; + + string? GetLibraryFoldersPath() + { + var steamInstallPath = GetSteamInstallationPath(); + return steamInstallPath == null ? null : Path.Combine(steamInstallPath, "steamapps", "libraryfolders.vdf"); + } + + [SupportedOSPlatform("windows")] + string? GetWindowsInstall() + { + const string steamRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Valve\Steam"; + const string steamRegistryValue = "InstallPath"; + + try + { + var installPath = Registry.GetValue(steamRegistryKey, steamRegistryValue, null); + + if (installPath != null) return installPath.ToString(); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to read Steam installation path from registry"); + } + + return null; + } + + [SupportedOSPlatform("macos")] + string? GetMacInstall() => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Steam"); + + string[] GetSteamLibraryPaths(string libraryFoldersVdfPath) + { + var pathRegex = LibraryLocations(); + var fileContent = File.ReadAllText(libraryFoldersVdfPath); + var matches = pathRegex.Matches(fileContent); + var libraryPaths = matches.Select(m => m.Groups[1].Value.Replace("\\\\", "\\")).ToList(); + return libraryPaths.Where(Directory.Exists).ToArray(); + } + + public void InstallIntegration() + { + var gameStateIntegrationPath = FindGameStateIntegrationPath(); + + if (gameStateIntegrationPath == null) + { + logger.LogError("gamestate_integration folder not found"); + return; + } + + var configFileFullPath = Path.Combine(gameStateIntegrationPath, ConfigFile); + var configFileSourcePath = Path.Combine(Directory.GetCurrentDirectory(), ConfigFile); + + if (File.Exists(configFileFullPath)) File.Delete(configFileFullPath); + + File.Copy(configFileSourcePath, configFileFullPath); + } + + [GeneratedRegex(@"\s*\""path\""\s*\""(.+?)\""", RegexOptions.Compiled)] + private static partial Regex LibraryLocations(); +} \ No newline at end of file diff --git a/src/Dota2Helper/Dota2Helper.csproj b/src/Dota2Helper/Dota2Helper.csproj index 3c2a07c..61fcc73 100644 --- a/src/Dota2Helper/Dota2Helper.csproj +++ b/src/Dota2Helper/Dota2Helper.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Dota2Helper/ViewModels/SettingsViewModel.cs b/src/Dota2Helper/ViewModels/SettingsViewModel.cs index 7c779ee..d8ffb74 100644 --- a/src/Dota2Helper/ViewModels/SettingsViewModel.cs +++ b/src/Dota2Helper/ViewModels/SettingsViewModel.cs @@ -7,6 +7,7 @@ using Avalonia.Styling; using Dota2Helper.Core.Audio; using Dota2Helper.Core.Configuration; +using Dota2Helper.Core.Gsi; using Dota2Helper.Core.Timers; using DynamicData.Binding; using ReactiveUI; @@ -15,10 +16,10 @@ namespace Dota2Helper.ViewModels; public class SettingsViewModel : ViewModelBase { - static readonly object WriterLock = new(); - + readonly static object WriterLock = new(); + readonly SteamLibraryService _steamLibraryService; readonly AudioPlayer _audioPlayer; public DotaTimers Timers { get; } @@ -34,8 +35,9 @@ public double Volume get => _audioPlayer.Volume; } - public SettingsViewModel(AudioPlayer audioPlayer, DotaTimers timers) + public SettingsViewModel(SteamLibraryService steamLibraryService, AudioPlayer audioPlayer, DotaTimers timers) { + _steamLibraryService = steamLibraryService; _audioPlayer = audioPlayer; Timers = timers; @@ -60,6 +62,14 @@ public SettingsViewModel(AudioPlayer audioPlayer, DotaTimers timers) nameof(DotaTimer.Reminder) ]; + public void Install() + { + _steamLibraryService.InstallIntegration(); + this.RaisePropertyChanged(nameof(IsIntegrated)); + } + + public bool IsIntegrated => _steamLibraryService.IsIntegrationInstalled(); + public void ToggleTheme() { var toggle = Application.Current!.RequestedThemeVariant switch diff --git a/src/Dota2Helper/ViewModels/WindowExtensions.cs b/src/Dota2Helper/ViewModels/WindowExtensions.cs index 379d203..b002930 100644 --- a/src/Dota2Helper/ViewModels/WindowExtensions.cs +++ b/src/Dota2Helper/ViewModels/WindowExtensions.cs @@ -35,7 +35,6 @@ internal static void HideMinimizeAndMaximizeButtons(this Window window) SetWindowLong(hwnd, GWL_STYLE, (currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX)); SetWindowLong(hwnd, GWL_EXSTYLE, (currentExStyle | WS_EX_DLGMODALFRAME)); - SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED); } } diff --git a/src/Dota2Helper/Views/SettingsView.axaml b/src/Dota2Helper/Views/SettingsView.axaml index 67e2432..b06c0a7 100644 --- a/src/Dota2Helper/Views/SettingsView.axaml +++ b/src/Dota2Helper/Views/SettingsView.axaml @@ -23,42 +23,62 @@ + + + - - + + + +