From da11f8e5b82c49171c7d68427a332233ce5e4397 Mon Sep 17 00:00:00 2001 From: Patrick Magee Date: Sat, 26 Oct 2024 18:12:30 +0100 Subject: [PATCH] - Add pull timer support (WIP) - Ignore properties that change frequently via timer calculations --- README.md | 16 ++++++ src/Dota2Helper.Desktop/appsettings.json | 25 ++++++++ src/Dota2Helper/App.axaml | 1 - src/Dota2Helper/App.axaml.cs | 7 +-- src/Dota2Helper/Core/Audio/AudioPlayer.cs | 23 +++++--- src/Dota2Helper/Core/Audio/AudioQueueItem.cs | 9 ++- .../Core/Configuration/TimeSpanConverter.cs | 29 ++++++++-- .../Core/Configuration/TimerOptions.cs | 3 + src/Dota2Helper/Core/Framework/ViewLocator.cs | 2 +- ...ameStateService.cs => GsiConfigService.cs} | 2 +- .../Core/Listeners/DotaListener.cs | 16 ++++-- src/Dota2Helper/Core/Timers/DotaTimer.cs | 57 +++++++++++++------ src/Dota2Helper/Core/Timers/DotaTimers.cs | 7 ++- .../ViewModels/SettingsViewModel.cs | 36 +++++++----- src/Dota2Helper/Views/SettingsView.axaml | 41 +++++++++---- 15 files changed, 201 insertions(+), 73 deletions(-) rename src/Dota2Helper/Core/Gsi/{GameStateService.cs => GsiConfigService.cs} (98%) diff --git a/README.md b/README.md index 89bd11e..83d7d9c 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,11 @@ Note on the fields: - `Reminder` - Reminder time (i.e 15 seconds before the timer) - `AudioFile` - Audio file path for effect sound if not using Text to speech - `Label` - Name of the timer +- `Offset` - Some timers can be offset from the minute mark. This is used to adjust the timer for these cases. +Offset example: + +Pulling the small camp at `0:15` and `0:45` is a common strategy in Dota 2. To set up a timer for this, you would set the `First` to `02:00`, the `Interval` to `00:30`, and the `Reminder` to `00:15`. The `Offset` would be `-00:15` to adjust the timer to `0:15` and `0:45`.
appsettings.json @@ -101,6 +105,18 @@ Note on the fields: "IsTts": false, "IsSoundEnabled": true }, + { + "Label": "Pull", + "First": "02:00", + "Interval": "00:30", + "Offset": "-00:15", + "Reminder": "00:15", + "AudioFile": "audio/Stack.mp3", + "IsManualReset": false, + "IsEnabled": true, + "IsTts": false, + "IsSoundEnabled": true + }, { "Label": "Wisdom", "First": "07:00", diff --git a/src/Dota2Helper.Desktop/appsettings.json b/src/Dota2Helper.Desktop/appsettings.json index 7053944..a038488 100644 --- a/src/Dota2Helper.Desktop/appsettings.json +++ b/src/Dota2Helper.Desktop/appsettings.json @@ -13,6 +13,31 @@ "IsSoundEnabled": true, "IsTts": true }, + { + "Speech": "Pull", + "Label": "Pull 1", + "First": "02:00", + "Interval": "01:00", + "Offset": "-00:15", + "Reminder": "00:15", + "AudioFile": "audio/Stack.mp3", + "IsManualReset": false, + "IsEnabled": false, + "IsSoundEnabled": true, + "IsTts": true + }, + { + "Speech": "Pull", + "Label": "Pull 2", + "First": "02:00", + "Interval": "00:45", + "Reminder": "00:15", + "AudioFile": "audio/Stack.mp3", + "IsManualReset": false, + "IsEnabled": false, + "IsSoundEnabled": true, + "IsTts": true + }, { "Speech": "Wisdom", "Label": "Wisdom", diff --git a/src/Dota2Helper/App.axaml b/src/Dota2Helper/App.axaml index 89df33b..e58de76 100644 --- a/src/Dota2Helper/App.axaml +++ b/src/Dota2Helper/App.axaml @@ -1,6 +1,5 @@ diff --git a/src/Dota2Helper/App.axaml.cs b/src/Dota2Helper/App.axaml.cs index 4e3e061..90be2d8 100644 --- a/src/Dota2Helper/App.axaml.cs +++ b/src/Dota2Helper/App.axaml.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; +using Avalonia.Controls; using Dota2Helper.Core.Audio; using Dota2Helper.Core.BackgroundServices; using Dota2Helper.Core.Configuration; @@ -17,8 +18,6 @@ using Dota2Helper.Core.Listeners; using Dota2Helper.Core.Timers; using Microsoft.Extensions.Logging; -using GameStateService = Dota2Helper.Core.Gsi.GameStateService; -using ViewLocator = Dota2Helper.Core.Framework.ViewLocator; using Hosting = Microsoft.Extensions.Hosting; namespace Dota2Helper; @@ -74,8 +73,8 @@ static IHost CreateHost() builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); builder.Services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); @@ -113,7 +112,7 @@ static IHost CreateHost() builder.Services.AddView(); builder.Services.AddView(); - builder.Services.AddHostedService(); + builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); diff --git a/src/Dota2Helper/Core/Audio/AudioPlayer.cs b/src/Dota2Helper/Core/Audio/AudioPlayer.cs index e9ef4af..d574c4b 100644 --- a/src/Dota2Helper/Core/Audio/AudioPlayer.cs +++ b/src/Dota2Helper/Core/Audio/AudioPlayer.cs @@ -10,12 +10,21 @@ namespace Dota2Helper.Core.Audio; [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class AudioPlayer : IDisposable { - private readonly static LibVLC LibVlc = new(); - private readonly MediaPlayer _player = new(LibVlc); - private readonly Queue _queue = new(); - private readonly SpeechSynthesizer _synthesizer = new(); + readonly static LibVLC LibVlc = new(); + readonly MediaPlayer _player = new(LibVlc); + readonly Queue _queue = new(); + readonly SpeechSynthesizer _synthesizer = new(); - public void QueueReminder(DotaTimer timer) => _queue.Enqueue(AudioQueueItem.FromTimer(timer)); + public void QueueReminder(DotaTimer timer) + { + // Prevent duplicate reminders + foreach (var item in _queue) + { + if (item.Label == timer.Label) return; + } + + _queue.Enqueue(AudioQueueItem.FromTimer(timer)); + } public int Volume { @@ -33,7 +42,7 @@ public void PlayReminder() { if (!_queue.TryDequeue(out var item)) return; - if (item.AudioQueueItemType == AudioQueueItemType.Audio) + if (item.AudioQueueItemType == AudioQueueItemType.Effect) { using (var reminderAudio = new Media(LibVlc, item.Value)) { @@ -41,7 +50,7 @@ public void PlayReminder() } } - if (item.AudioQueueItemType == AudioQueueItemType.Tts) + if (item.AudioQueueItemType == AudioQueueItemType.TextToSpeech) { _synthesizer.SpeakAsync(item.Value); } diff --git a/src/Dota2Helper/Core/Audio/AudioQueueItem.cs b/src/Dota2Helper/Core/Audio/AudioQueueItem.cs index e71951b..49be722 100644 --- a/src/Dota2Helper/Core/Audio/AudioQueueItem.cs +++ b/src/Dota2Helper/Core/Audio/AudioQueueItem.cs @@ -5,6 +5,8 @@ namespace Dota2Helper.Core.Audio; public class AudioQueueItem { public required string Value { get; init; } + + public required string Label { get; init; } public AudioQueueItemType AudioQueueItemType { get; init; } @@ -12,12 +14,13 @@ public class AudioQueueItem public static AudioQueueItem FromTimer(DotaTimer timer) => new() { Value = timer.IsTts ? timer.Speech : timer.AudioFile, - AudioQueueItemType = timer.IsTts ? AudioQueueItemType.Tts : AudioQueueItemType.Audio + AudioQueueItemType = timer.IsTts ? AudioQueueItemType.TextToSpeech : AudioQueueItemType.Effect, + Label = timer.Label }; } public enum AudioQueueItemType { - Tts, - Audio + TextToSpeech, + Effect } \ No newline at end of file diff --git a/src/Dota2Helper/Core/Configuration/TimeSpanConverter.cs b/src/Dota2Helper/Core/Configuration/TimeSpanConverter.cs index 5afd93b..2741451 100644 --- a/src/Dota2Helper/Core/Configuration/TimeSpanConverter.cs +++ b/src/Dota2Helper/Core/Configuration/TimeSpanConverter.cs @@ -5,22 +5,41 @@ namespace Dota2Helper.Core.Configuration; -public class TimeSpanConverter : JsonConverter +public class TimeSpanConverter : JsonConverter { public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var stringValue = reader.GetString(); - - if (TimeSpan.TryParseExact(stringValue, @"mm\:ss", CultureInfo.InvariantCulture, TimeSpanStyles.None, out var timeSpan)) + + if (stringValue == null) + { + return TimeSpan.Zero; + } + + bool isNegative = stringValue.StartsWith('-'); + + if (TimeSpan.TryParseExact(stringValue.TrimStart('-'), @"mm\:ss", CultureInfo.InvariantCulture, TimeSpanStyles.None, out var timeSpan)) { + if (isNegative) + { + timeSpan = timeSpan.Negate(); + } + return timeSpan; } - + throw new JsonException("Invalid TimeSpan format."); } public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) { - writer.WriteStringValue(value.ToString("mm\\:ss")); + if (value < TimeSpan.Zero) + { + writer.WriteStringValue("-" + value.ToString("mm\\:ss")); + } + else + { + writer.WriteStringValue(value.ToString("mm\\:ss")); + } } } \ No newline at end of file diff --git a/src/Dota2Helper/Core/Configuration/TimerOptions.cs b/src/Dota2Helper/Core/Configuration/TimerOptions.cs index 99afefe..77d64de 100644 --- a/src/Dota2Helper/Core/Configuration/TimerOptions.cs +++ b/src/Dota2Helper/Core/Configuration/TimerOptions.cs @@ -23,6 +23,9 @@ public class TimerOptions [JsonConverter(typeof(TimeSpanConverter))] public required TimeSpan Reminder { get; set; } + [JsonConverter(typeof(TimeSpanConverter))] + public TimeSpan Offset { get; set; } = TimeSpan.Zero; + /// /// The audio file for effect sound /// diff --git a/src/Dota2Helper/Core/Framework/ViewLocator.cs b/src/Dota2Helper/Core/Framework/ViewLocator.cs index 255e927..38ae692 100644 --- a/src/Dota2Helper/Core/Framework/ViewLocator.cs +++ b/src/Dota2Helper/Core/Framework/ViewLocator.cs @@ -8,7 +8,7 @@ namespace Dota2Helper.Core.Framework; public class ViewLocator : IDataTemplate { - private readonly Dictionary> _dic; + readonly Dictionary> _dic; public ViewLocator(IEnumerable descriptors) { diff --git a/src/Dota2Helper/Core/Gsi/GameStateService.cs b/src/Dota2Helper/Core/Gsi/GsiConfigService.cs similarity index 98% rename from src/Dota2Helper/Core/Gsi/GameStateService.cs rename to src/Dota2Helper/Core/Gsi/GsiConfigService.cs index 822a07a..d901f21 100644 --- a/src/Dota2Helper/Core/Gsi/GameStateService.cs +++ b/src/Dota2Helper/Core/Gsi/GsiConfigService.cs @@ -10,7 +10,7 @@ namespace Dota2Helper.Core.Gsi; -public partial class GameStateService(ILogger logger) +public partial class GsiConfigService(ILogger logger) { const string ConfigFile = "gamestate_integration_d2helper.cfg"; diff --git a/src/Dota2Helper/Core/Listeners/DotaListener.cs b/src/Dota2Helper/Core/Listeners/DotaListener.cs index 807d3a8..72100ab 100644 --- a/src/Dota2Helper/Core/Listeners/DotaListener.cs +++ b/src/Dota2Helper/Core/Listeners/DotaListener.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Avalonia.Controls; using Dota2Helper.Core.Configuration; using Dota2Helper.Core.Gsi; using Microsoft.Extensions.Logging; @@ -12,7 +13,7 @@ namespace Dota2Helper.Core.Listeners; -public class DotaListener(ILogger logger, IOptions settings, GameStateService gameStateService) : IDotaListener +public class DotaListener(ILogger logger, IOptions settings, GsiConfigService gsiConfig) : IDotaListener { HttpListener? _listener; @@ -33,9 +34,16 @@ public class DotaListener(ILogger logger, IOptions setti if (_listener == null) { - _listener = new HttpListener(); - _listener.Prefixes.Add(gameStateService.GetUri().ToString()); - _listener.Start(); + if (!Design.IsDesignMode) + { + _listener = new HttpListener(); + _listener.Prefixes.Add(gsiConfig.GetUri().ToString()); + _listener.Start(); + } + else + { + return null; + } } try diff --git a/src/Dota2Helper/Core/Timers/DotaTimer.cs b/src/Dota2Helper/Core/Timers/DotaTimer.cs index b326c34..ed93be4 100644 --- a/src/Dota2Helper/Core/Timers/DotaTimer.cs +++ b/src/Dota2Helper/Core/Timers/DotaTimer.cs @@ -5,13 +5,14 @@ namespace Dota2Helper.Core.Timers; public class DotaTimer : ReactiveObject { - private TimeSpan _manualResetTime; + TimeSpan _manualResetTime; protected internal DotaTimer( string label, TimeSpan first, TimeSpan interval, TimeSpan reminder, + TimeSpan offset, string audioFile, bool isManualReset, string speech, @@ -26,6 +27,7 @@ protected internal DotaTimer( AudioFile = audioFile; IsManualReset = isManualReset; Speech = speech; + Offset = offset; IsEnabled = isEnabled; IsSoundEnabled = isSoundEnabled; IsTts = isTts; @@ -37,7 +39,7 @@ protected internal DotaTimer( public string Speech { get; } - private bool _isTts; + bool _isTts; public bool IsTts { @@ -45,7 +47,7 @@ public bool IsTts set => this.RaiseAndSetIfChanged(ref _isTts, value); } - private TimeSpan _reminder; + TimeSpan _reminder; public TimeSpan Reminder { @@ -59,7 +61,13 @@ public int ReminderInSeconds set => Reminder = TimeSpan.FromSeconds(value); } - private bool _isEnabled; + public int OffsetInSeconds + { + get => (int) Offset.TotalSeconds; + set => Offset = TimeSpan.FromSeconds(value); + } + + bool _isEnabled; public bool IsEnabled { @@ -74,22 +82,28 @@ public bool IsEnabled public TimeSpan First { get; } public TimeSpan Interval { get; } - - private TimeSpan _timeRemaining; - - public TimeSpan TimeRemaining + TimeSpan _offset; + public TimeSpan Offset { - get => _timeRemaining; - private set + get => _offset; + set { this.RaisePropertyChanging(); - _timeRemaining = value; + _offset = value; this.RaisePropertyChanged(); this.RaisePropertyChanged(nameof(IsReminderActive)); } } - private bool _isActive; + TimeSpan _timeRemaining; + + public TimeSpan TimeRemaining + { + get => _timeRemaining; + private set => this.RaiseAndSetIfChanged(ref _timeRemaining, value); + } + + bool _isActive; public bool IsActive { @@ -98,7 +112,7 @@ public bool IsActive } - private bool _isSoundEnabled; + bool _isSoundEnabled; public bool IsSoundEnabled { @@ -112,9 +126,9 @@ public bool IsSoundEnabled public bool IsPendingManualReset => !IsActive && IsManualReset; - private bool _isSoundPlayed; + bool _isSoundPlayed; - private bool IsSoundPlayed + bool IsSoundPlayed { get => _isSoundPlayed; set => this.RaiseAndSetIfChanged(ref _isSoundPlayed, value); @@ -131,9 +145,16 @@ public void Reset() _manualResetTime = default; } - private TimeSpan GetObjectiveTime(TimeSpan gameTime) + TimeSpan GetObjectiveTime(TimeSpan gameTime) { - return gameTime > First ? Interval : First + (gameTime < TimeSpan.Zero ? gameTime.Negate() : TimeSpan.Zero); + var time = gameTime > First ? Interval : First + (gameTime < TimeSpan.Zero ? gameTime.Negate() : TimeSpan.Zero); + + if (Offset != TimeSpan.Zero) + { + time = time.Add(Offset); + } + + return time; } public void Update(TimeSpan gameTime) @@ -163,7 +184,7 @@ public void Update(TimeSpan gameTime) } } - private TimeSpan CalculateTimeRemaining(TimeSpan gameTime) + TimeSpan CalculateTimeRemaining(TimeSpan gameTime) { var interval = GetObjectiveTime(gameTime); diff --git a/src/Dota2Helper/Core/Timers/DotaTimers.cs b/src/Dota2Helper/Core/Timers/DotaTimers.cs index d99723a..2d349db 100644 --- a/src/Dota2Helper/Core/Timers/DotaTimers.cs +++ b/src/Dota2Helper/Core/Timers/DotaTimers.cs @@ -20,8 +20,9 @@ public DotaTimers(IOptions settings) item.Label, item.First, item.Interval, - item.Reminder, - item.AudioFile, + item.Reminder, + item.Offset, + item.AudioFile, item.IsManualReset, item.Speech, item.IsTts, @@ -29,7 +30,7 @@ public DotaTimers(IOptions settings) item.IsEnabled)); } - foreach (var timer in timers.OrderBy(x => x.Interval)) + foreach (var timer in timers) { Add(timer); } diff --git a/src/Dota2Helper/ViewModels/SettingsViewModel.cs b/src/Dota2Helper/ViewModels/SettingsViewModel.cs index 0693f56..62160be 100644 --- a/src/Dota2Helper/ViewModels/SettingsViewModel.cs +++ b/src/Dota2Helper/ViewModels/SettingsViewModel.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Styling; using Dota2Helper.Core.Audio; +using Dota2Helper.Core.BackgroundServices; using Dota2Helper.Core.Configuration; using Dota2Helper.Core.Gsi; using Dota2Helper.Core.Timers; @@ -16,7 +17,7 @@ public class SettingsViewModel : ViewModelBase { readonly static object WriterLock = new(); - readonly GameStateService _gameStateService; + readonly GsiConfigService _gsiConfigService; readonly AudioPlayer _audioPlayer; public DotaTimers Timers { get; } @@ -32,9 +33,9 @@ public double Volume get => _audioPlayer.Volume; } - public SettingsViewModel(GameStateService gameStateService, AudioPlayer audioPlayer, DotaTimers timers) + public SettingsViewModel(GsiConfigService gsiConfigService, AudioPlayer audioPlayer, DotaTimers timers) { - _gameStateService = gameStateService; + _gsiConfigService = gsiConfigService; _audioPlayer = audioPlayer; Timers = timers; @@ -51,27 +52,25 @@ public SettingsViewModel(GameStateService gameStateService, AudioPlayer audioPla public bool IsSpeakerOn => Volume > 0; - ImmutableArray Properties => + ImmutableArray IgnoredProperties => [ - nameof(DotaTimer.IsEnabled), - nameof(DotaTimer.IsSoundEnabled), - nameof(DotaTimer.IsTts), - nameof(DotaTimer.Reminder) + nameof(DotaTimer.TimeRemaining), + nameof(DotaTimer.IsActive), ]; public void Install() { - _gameStateService.InstallIntegration(); + _gsiConfigService.InstallIntegration(); this.RaisePropertyChanged(nameof(IsIntegrated)); } // Open folder with steam dota2 install public void Open() { - _gameStateService.OpenGameStateIntegrationFolder(); + _gsiConfigService.OpenGameStateIntegrationFolder(); } - public bool IsIntegrated => _gameStateService.IsIntegrationInstalled(); + public bool IsIntegrated => _gsiConfigService.IsIntegrationInstalled(); public void ToggleTheme() { @@ -90,12 +89,12 @@ public void ToggleTheme() public int? PortNumber { - get => _gameStateService.GetPortNumber(); + get => _gsiConfigService.GetPortNumber(); set { if (value != _portNumber) { - _gameStateService.SetPortNumber(value!.Value); + _gsiConfigService.SetPortNumber(value!.Value); } this.RaiseAndSetIfChanged(ref _portNumber, value); @@ -110,7 +109,7 @@ public string? ThemeName set => this.RaiseAndSetIfChanged(ref _themeName, value); } - private bool _isDotaListener; + bool _isDotaListener; public bool IsDotaListener { @@ -120,8 +119,13 @@ public bool IsDotaListener void TimerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { + if(e.PropertyName is null || IgnoredProperties.Contains(e.PropertyName)) + { + return; + } + // Update the appsettings.json file when a valid timer property changes - if (sender is DotaTimer timer && e.PropertyName is not null && Properties.Contains(e.PropertyName)) + if (sender is DotaTimer timer && e.PropertyName is not null) { lock (WriterLock) { @@ -136,7 +140,9 @@ void TimerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) item.First = timer.First; item.Interval = timer.Interval; item.Reminder = timer.Reminder; + item.Offset = timer.Offset; item.AudioFile = timer.AudioFile; + item.Speech = timer.Speech; item.IsManualReset = timer.IsManualReset; item.IsSoundEnabled = timer.IsSoundEnabled; item.IsEnabled = timer.IsEnabled; diff --git a/src/Dota2Helper/Views/SettingsView.axaml b/src/Dota2Helper/Views/SettingsView.axaml index 854054c..47c5ce7 100644 --- a/src/Dota2Helper/Views/SettingsView.axaml +++ b/src/Dota2Helper/Views/SettingsView.axaml @@ -26,6 +26,7 @@ + @@ -53,6 +54,7 @@ + @@ -62,16 +64,16 @@ @@ -104,10 +106,9 @@ Classes.IsReminderActive="{Binding Path=IsReminderActive}" Text="{Binding Path=TimeRemaining, StringFormat=mm\\:ss}" /> - + @@ -139,17 +140,35 @@ Width="110" DockPanel.Dock="Right" Increment="1" - Minimum="5" + Minimum="-60" + AllowSpin="True" + Maximum="60" + ParsingNumberStyle="Integer" + ToolTip.Placement="Left" + ToolTip.Tip="Reminder trigger Mouse Scroll Up/Down"> + + + + ToolTip.Tip="Tweak Offset Mouse Scroll Up/Down"> + + + @@ -157,7 +176,7 @@ -