diff --git a/CS-Jukebox/AdditionalSongsForm.cs b/CS-Jukebox/AdditionalSongsForm.cs index 317915f..a2b1b9d 100644 --- a/CS-Jukebox/AdditionalSongsForm.cs +++ b/CS-Jukebox/AdditionalSongsForm.cs @@ -9,6 +9,7 @@ namespace CS_Jukebox /// Edits the optional tracks for one game event. public class AdditionalSongsForm : Form { + private readonly MusicEventType eventType; private readonly List songs; private readonly ListBox songsList = new ListBox(); private readonly TextBox pathTextBox = new TextBox(); @@ -16,20 +17,22 @@ public class AdditionalSongsForm : Form private readonly TextBox startMinutesTextBox = new TextBox(); private readonly TextBox startSecondsTextBox = new TextBox(); private readonly Jukebox previewJukebox = new Jukebox(); + private readonly Button previewButton = new Button(); private int selectedIndex = -1; private bool refreshingSongList; private SongProfile previewSong; public List Songs => songs; - public AdditionalSongsForm(string eventName, IEnumerable currentSongs) + public AdditionalSongsForm(MusicEventType eventType, IEnumerable currentSongs) { + this.eventType = eventType; songs = currentSongs? .Where(song => song != null) .Select(song => new SongProfile(song.Path, song.Volume) { Start = song.Start, NormalizationGain = song.NormalizationGain }) .ToList() ?? new List(); - Text = eventName + " - Extra tracks"; + Text = MusicEventTiming.GetDisplayName(eventType) + " - Extra tracks"; Width = 580; Height = 330; MinimizeBox = false; @@ -38,7 +41,9 @@ public AdditionalSongsForm(string eventName, IEnumerable currentSon BuildControls(); RefreshSongList(); - FormClosed += (sender, args) => previewJukebox.Dispose(); + previewJukebox.PreviewCompleted += PreviewJukebox_PreviewCompleted; + FormClosed += AdditionalSongsForm_FormClosed; + ThemeManager.Apply(this); } private void BuildControls() @@ -53,7 +58,8 @@ private void BuildControls() var pathLabel = new Label { Text = "Audio file:", Left = 240, Top = 18, AutoSize = true }; pathTextBox.SetBounds(240, 40, 180, 23); - var previewButton = new Button { Text = "▶", Left = 428, Top = 39, Width = 30, Height = 28 }; + previewButton.Text = "▶"; + previewButton.SetBounds(428, 39, 30, 28); previewButton.Click += (sender, args) => TogglePreview(previewButton); var browseButton = new Button { Text = "Browse...", Left = 478, Top = 39, Width = 78 }; browseButton.Click += (sender, args) => BrowseForSong(); @@ -180,7 +186,10 @@ private void TogglePreview(Button previewButton) return; } - previewJukebox.PlayPreviewSong(song); + if (Properties.LimitPreviewToEventDuration) + previewJukebox.PlayPreviewSong(song, MusicEventTiming.GetPreviewDurationSeconds(eventType)); + else + previewJukebox.PlayPreviewSong(song); previewSong = song; previewButton.Text = "■"; } @@ -191,6 +200,24 @@ private void TogglePreview(Button previewButton) } } + private void PreviewJukebox_PreviewCompleted(object sender, PreviewCompletedEventArgs e) + { + previewSong = null; + previewButton.Text = "▶"; + + if (IsDisposed || Disposing) return; + string message = e.Reason == PreviewCompletionReason.EventEnded + ? "Event ended before music." + : "Music ended before event."; + MessageBox.Show(this, message, "Preview complete", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void AdditionalSongsForm_FormClosed(object sender, FormClosedEventArgs e) + { + previewJukebox.PreviewCompleted -= PreviewJukebox_PreviewCompleted; + previewJukebox.Dispose(); + } + private void VolumeTrackBar_ValueChanged(object sender, EventArgs e) { if (previewSong == null || !previewJukebox.IsPlaybackActive() || diff --git a/CS-Jukebox/CS-Jukebox.csproj b/CS-Jukebox/CS-Jukebox.csproj index fa07c6d..0f5d2c3 100644 --- a/CS-Jukebox/CS-Jukebox.csproj +++ b/CS-Jukebox/CS-Jukebox.csproj @@ -7,14 +7,27 @@ true CS_Jukebox.Program logo.ico + 1.3.3 + 1.3.3.0 + 1.3.3.0 win-x64 true true + + true + true + true false + + none + false + false + + @@ -25,9 +38,8 @@ - - Always - + diff --git a/CS-Jukebox/GameLogic.cs b/CS-Jukebox/GameLogic.cs index 8fc18af..6b34240 100644 --- a/CS-Jukebox/GameLogic.cs +++ b/CS-Jukebox/GameLogic.cs @@ -143,9 +143,10 @@ void OnNewGameState(GameState gs) if (gs.Round.Phase == RoundPhase.Live && musicState != MusicState.Live && musicState != MusicState.BombPlanted) { jukebox.Stop(); - // Play the round-start track to completion, matching the behaviour - // of FreezeTime and MVP which also use the two-arg overload. - if (PlaySongIfValid(Properties.SelectedKit.PickSong(Properties.SelectedKit.startSong, Properties.SelectedKit.startSongs), false)) + if (PlaySongIfValid( + Properties.SelectedKit.PickSong(Properties.SelectedKit.startSong, Properties.SelectedKit.startSongs), + false, + MusicEventTiming.RoundStartSeconds)) { musicState = MusicState.Live; Logger.LogEvent("TransitionToLive", $"SelectedKit={Properties.SelectedKit.Name}"); @@ -240,7 +241,7 @@ private void HandlePlayerDeath(GameState gs) if (PlaySongIfValid( Properties.SelectedKit.PickSong(Properties.SelectedKit.deathSong, Properties.SelectedKit.deathSongs), false, - 5)) + MusicEventTiming.PlayerDeathSeconds)) { Console.WriteLine("Local player died"); Logger.LogEvent("PlayerDeathDetected"); diff --git a/CS-Jukebox/GamePathForm.cs b/CS-Jukebox/GamePathForm.cs index d85db52..ddab41b 100644 --- a/CS-Jukebox/GamePathForm.cs +++ b/CS-Jukebox/GamePathForm.cs @@ -23,6 +23,7 @@ public GamePathForm() MinimizeBox = false; if (!string.IsNullOrWhiteSpace(Properties.GameDir)) dirTextBox.Text = Properties.GameDir; + ThemeManager.Apply(this); } //Open folder browser dialog diff --git a/CS-Jukebox/Jukebox.cs b/CS-Jukebox/Jukebox.cs index 3c5ecc0..274b92c 100644 --- a/CS-Jukebox/Jukebox.cs +++ b/CS-Jukebox/Jukebox.cs @@ -22,6 +22,10 @@ public class Jukebox : IDisposable private bool isPlaying = false; private long scheduledStopAtMilliseconds; + private bool previewCompletionPending; + private long previewStopAtMilliseconds; + + public event EventHandler PreviewCompleted; public Jukebox() { @@ -110,7 +114,24 @@ public void PlaySong(SongProfile song, bool loop) public void PlayPreviewSong(SongProfile song) { - Logger.LogEntry($"song={song?.Path}"); + PlayPreviewSongInternal(song, null, false); + } + + /// + /// Plays a preview and reports why it completed. A null duration keeps + /// the preview unbounded but still reports when the source file ends. + /// + public void PlayPreviewSong(SongProfile song, int? durationSeconds) + { + if (durationSeconds.HasValue && durationSeconds.Value <= 0) + throw new ArgumentOutOfRangeException(nameof(durationSeconds)); + + PlayPreviewSongInternal(song, durationSeconds, true); + } + + private void PlayPreviewSongInternal(SongProfile song, int? durationSeconds, bool reportCompletion) + { + Logger.LogEntry($"song={song?.Path}, duration={durationSeconds?.ToString() ?? "unbounded"}, reportCompletion={reportCompletion}"); if (song == null || string.IsNullOrWhiteSpace(song.Path) || !File.Exists(song.Path)) { Logger.Log($"WARNING: Song file path is missing or invalid: '{song?.Path}'"); @@ -121,6 +142,7 @@ public void PlayPreviewSong(SongProfile song) lock (lockObj) { CancelScheduledStopInternal(); + CancelPreviewCompletionInternal(); CancelFadeInternal(); previewMode = true; currentSong = song; @@ -144,12 +166,17 @@ public void PlayPreviewSong(SongProfile song) outputDevice = new WaveOutEvent(); outputDevice.Init(waveStream); outputDevice.Play(); + previewCompletionPending = reportCompletion; + previewStopAtMilliseconds = durationSeconds.HasValue + ? Environment.TickCount64 + (durationSeconds.Value * 1000L) + : 0; songTimer.Start(); Logger.LogExit("Preview playback started successfully"); } catch (Exception ex) { Logger.LogError("PlayPreviewSong failed", ex); + CancelPreviewCompletionInternal(); CleanupOutput(); throw new InvalidOperationException("The selected audio file could not be played.", ex); } @@ -272,6 +299,7 @@ public void Stop() lock (lockObj) { CancelScheduledStopInternal(); + CancelPreviewCompletionInternal(); if (!IsPlaybackActive()) { CleanupOutput(); @@ -347,6 +375,12 @@ private void CancelScheduledStopInternal() scheduledStopAtMilliseconds = 0; } + private void CancelPreviewCompletionInternal() + { + previewCompletionPending = false; + previewStopAtMilliseconds = 0; + } + private void CancelFadeInternal() { CancellationTokenSource cts; @@ -364,6 +398,7 @@ public void StopImmediately() lock (lockObj) { CancelScheduledStopInternal(); + CancelPreviewCompletionInternal(); CancelFadeInternal(); CleanupOutput(); } @@ -452,11 +487,28 @@ private void TimerTick(object sender, EventArgs e) if (outputDevice.PlaybackState == PlaybackState.Stopped && fadeCts == null) { + bool reportMusicEnded = previewCompletionPending; CancelScheduledStopInternal(); + CancelPreviewCompletionInternal(); lock (lockObj) { CleanupOutput(); } + if (reportMusicEnded) + OnPreviewCompleted(PreviewCompletionReason.MusicEnded); + return; + } + + if (previewCompletionPending && previewStopAtMilliseconds > 0 && + Environment.TickCount64 >= previewStopAtMilliseconds) + { + CancelScheduledStopInternal(); + CancelPreviewCompletionInternal(); + lock (lockObj) + { + CleanupOutput(); + } + OnPreviewCompleted(PreviewCompletionReason.EventEnded); return; } @@ -469,6 +521,18 @@ private void TimerTick(object sender, EventArgs e) UpdateVolume(); } + private void OnPreviewCompleted(PreviewCompletionReason reason) + { + try + { + PreviewCompleted?.Invoke(this, new PreviewCompletedEventArgs(reason)); + } + catch (Exception ex) + { + Logger.LogError("PreviewCompleted handler failed", ex); + } + } + private static bool IsGameFocused() { try diff --git a/CS-Jukebox/MainForm.Designer.cs b/CS-Jukebox/MainForm.Designer.cs index 6cd2a16..7d9640d 100644 --- a/CS-Jukebox/MainForm.Designer.cs +++ b/CS-Jukebox/MainForm.Designer.cs @@ -39,6 +39,7 @@ private void InitializeComponent() this.editButton = new System.Windows.Forms.Button(); this.notifyIcon = new System.Windows.Forms.NotifyIcon(this.components); this.autoCheckBox = new System.Windows.Forms.CheckBox(); + this.darkThemeCheckBox = new System.Windows.Forms.CheckBox(); ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).BeginInit(); this.SuspendLayout(); // @@ -131,19 +132,31 @@ private void InitializeComponent() // autoCheckBox // this.autoCheckBox.AutoSize = true; - this.autoCheckBox.Location = new System.Drawing.Point(139, 107); + this.autoCheckBox.Location = new System.Drawing.Point(139, 133); this.autoCheckBox.Name = "autoCheckBox"; this.autoCheckBox.Size = new System.Drawing.Size(138, 19); - this.autoCheckBox.TabIndex = 7; + this.autoCheckBox.TabIndex = 10; this.autoCheckBox.Text = "Autostart CS Jukebox"; this.autoCheckBox.UseVisualStyleBackColor = true; this.autoCheckBox.CheckedChanged += new System.EventHandler(this.autoCheckBox_CheckedChanged); - // + // + // darkThemeCheckBox + // + this.darkThemeCheckBox.AutoSize = true; + this.darkThemeCheckBox.Location = new System.Drawing.Point(139, 106); + this.darkThemeCheckBox.Name = "darkThemeCheckBox"; + this.darkThemeCheckBox.Size = new System.Drawing.Size(88, 19); + this.darkThemeCheckBox.TabIndex = 7; + this.darkThemeCheckBox.Text = "Dark theme"; + this.darkThemeCheckBox.UseVisualStyleBackColor = true; + this.darkThemeCheckBox.CheckedChanged += new System.EventHandler(this.darkThemeCheckBox_CheckedChanged); + // // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(285, 135); + this.ClientSize = new System.Drawing.Size(285, 162); + this.Controls.Add(this.darkThemeCheckBox); this.Controls.Add(this.autoCheckBox); this.Controls.Add(this.importButton); this.Controls.Add(this.exportButton); @@ -175,6 +188,7 @@ private void InitializeComponent() private System.Windows.Forms.Button editButton; private System.Windows.Forms.NotifyIcon notifyIcon; private System.Windows.Forms.CheckBox autoCheckBox; + private System.Windows.Forms.CheckBox darkThemeCheckBox; } } diff --git a/CS-Jukebox/MainForm.cs b/CS-Jukebox/MainForm.cs index d0bb24e..30a4f3a 100644 --- a/CS-Jukebox/MainForm.cs +++ b/CS-Jukebox/MainForm.cs @@ -18,6 +18,7 @@ public partial class MainForm : Form static extern bool AllocConsole(); private GameLogic logic; + private bool initializingTheme; public MainForm() { @@ -26,9 +27,11 @@ public MainForm() //AllocConsole(); //Enable console MaximizeBox = false; - // Use default system theme (no custom theming applied) - Properties.Load(); + initializingTheme = true; + darkThemeCheckBox.Checked = Properties.DarkTheme; + initializingTheme = false; + ThemeManager.Apply(this); // If GameDir is missing or invalid, attempt auto-detection of CS2 install. bool gameDirValid = false; @@ -559,6 +562,15 @@ private void autoCheckBox_CheckedChanged(object sender, EventArgs e) RegisterInStartup(autoCheckBox.Checked); } + private void darkThemeCheckBox_CheckedChanged(object sender, EventArgs e) + { + if (initializingTheme) return; + + Properties.DarkTheme = darkThemeCheckBox.Checked; + ThemeManager.ApplyToOpenForms(); + Properties.SaveProperties(); + } + private async void exportButton_Click(object sender, EventArgs e) { if (Properties.SelectedKit == null) diff --git a/CS-Jukebox/MusicEventTiming.cs b/CS-Jukebox/MusicEventTiming.cs new file mode 100644 index 0000000..8e91011 --- /dev/null +++ b/CS-Jukebox/MusicEventTiming.cs @@ -0,0 +1,66 @@ +namespace CS_Jukebox +{ + public enum MusicEventType + { + MainMenu, + FreezeTime, + RoundStart, + BombPlanted, + BombTenSeconds, + RoundTenSeconds, + Mvp, + RoundWon, + RoundLost, + PlayerDeath + } + + /// + /// Fixed preview windows documented by the project. These values describe + /// the length of the event preview after the configured Start At offset. + /// + public static class MusicEventTiming + { + public const int FreezeTimeSeconds = 15; + public const int RoundStartSeconds = 5; + public const int BombPlantedSeconds = 30; + public const int TenSecondWarningSeconds = 10; + public const int RoundResultSeconds = 7; + public const int PlayerDeathSeconds = 5; + + public static int? GetPreviewDurationSeconds(MusicEventType eventType) + { + return eventType switch + { + MusicEventType.FreezeTime => FreezeTimeSeconds, + MusicEventType.RoundStart => RoundStartSeconds, + MusicEventType.BombPlanted => BombPlantedSeconds, + MusicEventType.BombTenSeconds => TenSecondWarningSeconds, + MusicEventType.RoundTenSeconds => TenSecondWarningSeconds, + MusicEventType.Mvp => RoundResultSeconds, + MusicEventType.RoundWon => RoundResultSeconds, + MusicEventType.RoundLost => RoundResultSeconds, + MusicEventType.PlayerDeath => PlayerDeathSeconds, + MusicEventType.MainMenu => null, + _ => null + }; + } + + public static string GetDisplayName(MusicEventType eventType) + { + return eventType switch + { + MusicEventType.MainMenu => "Main Menu", + MusicEventType.FreezeTime => "Freeze Time", + MusicEventType.RoundStart => "Round Start", + MusicEventType.BombPlanted => "Bomb Planted", + MusicEventType.BombTenSeconds => "Bomb: 10 seconds", + MusicEventType.RoundTenSeconds => "Round: 10 seconds", + MusicEventType.Mvp => "MVP", + MusicEventType.RoundWon => "Round Won", + MusicEventType.RoundLost => "Round Lost", + MusicEventType.PlayerDeath => "Player Death", + _ => eventType.ToString() + }; + } + } +} diff --git a/CS-Jukebox/MusicSelector.cs b/CS-Jukebox/MusicSelector.cs index f829583..c2dbeba 100644 --- a/CS-Jukebox/MusicSelector.cs +++ b/CS-Jukebox/MusicSelector.cs @@ -13,6 +13,7 @@ public partial class MusicSelector : Form private Jukebox previewJukebox; private SongProfile previewSong; private TrackBar previewVolumeTrackBar; + private MusicEventType? previewEventType; private readonly Dictionary startTimeControls = new(); private readonly GroupBox deathGroup = new GroupBox(); private readonly Label deathStartLabel = new Label(); @@ -22,12 +23,14 @@ public partial class MusicSelector : Form private readonly Button deathPreviewButton = new Button(); private readonly TextBox deathTextBox = new TextBox(); private readonly Button saveAsCopyButton = new Button(); + private readonly CheckBox previewDurationCheckBox = new CheckBox(); public MusicSelector(MusicKit newKit, bool? createKit) { InitializeComponent(); CreateDeathScenarioControls(); CreateStartTimeControls(); + CreatePreviewDurationControl(); ConfigureEditorLayout(); MaximizeBox = false; @@ -48,10 +51,13 @@ public MusicSelector(MusicKit newKit, bool? createKit) saveAsCopyButton.Size = new System.Drawing.Size(110, 27); saveAsCopyButton.Click += saveAsCopyButton_Click; Controls.Add(saveAsCopyButton); + saveAsCopyButton.Visible = !createMode; // Jukebox used for previewing individual tracks inside the editor previewJukebox = new Jukebox(); + previewJukebox.PreviewCompleted += PreviewJukebox_PreviewCompleted; FormClosed += MusicSelector_FormClosed; + ThemeManager.Apply(this); } private void MusicSelector_Load(object sender, EventArgs e) @@ -96,7 +102,7 @@ private void CreateDeathScenarioControls() deathPreviewButton.Name = "deathPreviewButton"; deathPreviewButton.Text = "▶"; deathPreviewButton.Click += (sender, args) => - TogglePreview(deathTextBox, deathTrackBar, deathStartTextBox, deathPreviewButton); + TogglePreview(deathTextBox, deathTrackBar, deathStartTextBox, deathPreviewButton, MusicEventType.PlayerDeath); deathTextBox.Name = "deathTextBox"; @@ -108,6 +114,16 @@ private void CreateDeathScenarioControls() Controls.Add(deathGroup); } + private void CreatePreviewDurationControl() + { + previewDurationCheckBox.Name = "previewDurationCheckBox"; + previewDurationCheckBox.Text = "Limit preview to event duration"; + previewDurationCheckBox.AutoSize = true; + previewDurationCheckBox.Checked = Properties.LimitPreviewToEventDuration; + previewDurationCheckBox.CheckedChanged += PreviewDurationCheckBox_CheckedChanged; + Controls.Add(previewDurationCheckBox); + } + private void AddStartTimeControls(GroupBox group, Label label, TextBox legacyTextBox) { legacyTextBox.Visible = false; @@ -144,6 +160,7 @@ private void ConfigureEditorLayout() nameTextBox.Location = new System.Drawing.Point(62, 11); nameTextBox.Size = new System.Drawing.Size(200, 23); deleteButton.Location = new System.Drawing.Point(272, 10); + previewDurationCheckBox.Location = new System.Drawing.Point(370, 13); saveButton.Text = "Apply Changes"; saveButton.Size = new System.Drawing.Size(110, 27); saveButton.Location = new System.Drawing.Point(12, 625); @@ -205,9 +222,37 @@ private void EnsurePreviewButtonsVisible() private void MusicSelector_FormClosed(object sender, FormClosedEventArgs e) { + if (previewJukebox != null) + previewJukebox.PreviewCompleted -= PreviewJukebox_PreviewCompleted; previewJukebox?.Dispose(); } + private void PreviewDurationCheckBox_CheckedChanged(object sender, EventArgs e) + { + Properties.LimitPreviewToEventDuration = previewDurationCheckBox.Checked; + Properties.SaveProperties(); + + previewJukebox?.StopImmediately(); + previewSong = null; + previewVolumeTrackBar = null; + previewEventType = null; + ResetPreviewButtonTexts(); + } + + private void PreviewJukebox_PreviewCompleted(object sender, PreviewCompletedEventArgs e) + { + previewSong = null; + previewVolumeTrackBar = null; + previewEventType = null; + ResetPreviewButtonTexts(); + + if (IsDisposed || Disposing) return; + string message = e.Reason == PreviewCompletionReason.EventEnded + ? "Event ended." + : "Music ended."; + MessageBox.Show(this, message, "Preview complete", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + private void ConfigurePreviewVolumeSynchronization() { foreach (TrackBar trackBar in new[] @@ -233,19 +278,19 @@ private void PreviewVolumeTrackBar_ValueChanged(object sender, EventArgs e) private void AddExtraSongsButtons() { - AddExtraSongsButton(freezeGroup, "Freeze Time", () => currentKit.freezeSongs, songs => currentKit.freezeSongs = songs); - AddExtraSongsButton(startGroup, "Round Start", () => currentKit.startSongs, songs => currentKit.startSongs = songs); - AddExtraSongsButton(bombGroup, "Bomb Planted", () => currentKit.bombSongs, songs => currentKit.bombSongs = songs); - AddExtraSongsButton(wonGroup, "Round Won", () => currentKit.winSongs, songs => currentKit.winSongs = songs); - AddExtraSongsButton(lostGroup, "Round Lost", () => currentKit.loseSongs, songs => currentKit.loseSongs = songs); - AddExtraSongsButton(MVPGroup, "MVP", () => currentKit.MVPSongs, songs => currentKit.MVPSongs = songs); - AddExtraSongsButton(bombTenSecBox1, "Bomb: 10 seconds", () => currentKit.bombTenSecSongs, songs => currentKit.bombTenSecSongs = songs); - AddExtraSongsButton(roundTenSecBox, "Round: 10 seconds", () => currentKit.roundTenSecSongs, songs => currentKit.roundTenSecSongs = songs); - AddExtraSongsButton(mainMenuGroupBox, "Main Menu", () => currentKit.mainMenuSongs, songs => currentKit.mainMenuSongs = songs); - AddExtraSongsButton(deathGroup, "Player Death", () => currentKit.deathSongs, songs => currentKit.deathSongs = songs); + AddExtraSongsButton(freezeGroup, MusicEventType.FreezeTime, () => currentKit.freezeSongs, songs => currentKit.freezeSongs = songs); + AddExtraSongsButton(startGroup, MusicEventType.RoundStart, () => currentKit.startSongs, songs => currentKit.startSongs = songs); + AddExtraSongsButton(bombGroup, MusicEventType.BombPlanted, () => currentKit.bombSongs, songs => currentKit.bombSongs = songs); + AddExtraSongsButton(wonGroup, MusicEventType.RoundWon, () => currentKit.winSongs, songs => currentKit.winSongs = songs); + AddExtraSongsButton(lostGroup, MusicEventType.RoundLost, () => currentKit.loseSongs, songs => currentKit.loseSongs = songs); + AddExtraSongsButton(MVPGroup, MusicEventType.Mvp, () => currentKit.MVPSongs, songs => currentKit.MVPSongs = songs); + AddExtraSongsButton(bombTenSecBox1, MusicEventType.BombTenSeconds, () => currentKit.bombTenSecSongs, songs => currentKit.bombTenSecSongs = songs); + AddExtraSongsButton(roundTenSecBox, MusicEventType.RoundTenSeconds, () => currentKit.roundTenSecSongs, songs => currentKit.roundTenSecSongs = songs); + AddExtraSongsButton(mainMenuGroupBox, MusicEventType.MainMenu, () => currentKit.mainMenuSongs, songs => currentKit.mainMenuSongs = songs); + AddExtraSongsButton(deathGroup, MusicEventType.PlayerDeath, () => currentKit.deathSongs, songs => currentKit.deathSongs = songs); } - private void AddExtraSongsButton(GroupBox group, string eventName, + private void AddExtraSongsButton(GroupBox group, MusicEventType eventType, Func> getSongs, Action> setSongs) { var button = new Button @@ -263,8 +308,9 @@ private void AddExtraSongsButton(GroupBox group, string eventName, previewJukebox.StopImmediately(); previewSong = null; previewVolumeTrackBar = null; + previewEventType = null; ResetPreviewButtonTexts(); - using var editor = new AdditionalSongsForm(eventName, getSongs()); + using var editor = new AdditionalSongsForm(eventType, getSongs()); if (editor.ShowDialog(this) == DialogResult.OK) { setSongs(editor.Songs); @@ -502,7 +548,8 @@ private void menuButton_Click(object sender, EventArgs e) } // Preview button handlers - private void TogglePreview(TextBox textBox, TrackBar volumeTrackBar, TextBox startTextBox, Button previewButton) + private void TogglePreview(TextBox textBox, TrackBar volumeTrackBar, TextBox startTextBox, + Button previewButton, MusicEventType eventType) { string path = textBox.Text; if (string.IsNullOrWhiteSpace(path)) @@ -517,19 +564,24 @@ private void TogglePreview(TextBox textBox, TrackBar volumeTrackBar, TextBox sta // If these exact parameters are already playing, stop preview. if (previewSong != null && previewSong.Path == song.Path && previewSong.Volume == song.Volume && - previewSong.Start == song.Start && previewJukebox.IsPlaybackActive()) + previewSong.Start == song.Start && previewEventType == eventType && previewJukebox.IsPlaybackActive()) { previewJukebox.Stop(); previewSong = null; previewVolumeTrackBar = null; + previewEventType = null; previewButton.Text = "▶"; return; } // Preview exactly what will play in-game: volume and offset. - previewJukebox.PlayPreviewSong(song); + if (Properties.LimitPreviewToEventDuration) + previewJukebox.PlayPreviewSong(song, MusicEventTiming.GetPreviewDurationSeconds(eventType)); + else + previewJukebox.PlayPreviewSong(song); previewSong = song; previewVolumeTrackBar = volumeTrackBar; + previewEventType = eventType; ResetPreviewButtonTexts(); @@ -556,15 +608,15 @@ private void ResetPreviewButtonTexts() } } - private void freezePreviewButton_Click(object sender, EventArgs e) => TogglePreview(freezeTextBox, freezeTrackBar, freezeStartTextBox, freezePreviewButton); - private void startPreviewButton_Click(object sender, EventArgs e) => TogglePreview(startTextBox, startTrackBar, startStartTextBox, startPreviewButton); - private void bombPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTextBox, bombTrackBar, bombStartTextBox, bombPreviewButton); - private void wonPreviewButton_Click(object sender, EventArgs e) => TogglePreview(wonTextBox, wonTrackBar, wonStartTextBox, wonPreviewButton); - private void lostPreviewButton_Click(object sender, EventArgs e) => TogglePreview(lostTextBox, lostTrackBar, lostStartTextBox, lostPreviewButton); - private void MVPPreviewButton_Click(object sender, EventArgs e) => TogglePreview(MVPTextBox, MVPTrackBar, MVPStartTextBox, MVPPreviewButton); - private void bombTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTenSecTextBox, bombTenSecTrackBar, bombTenSecStartBox, bombTenSecPreviewButton); - private void roundTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(roundTenSecTextBox, roundTenSecTrackBar, roundTenSecStartBox, roundTenSecPreviewButton); - private void menuPreviewButton_Click(object sender, EventArgs e) => TogglePreview(menuTextBox, menuTrackBar, menuStartTextBox, menuPreviewButton); + private void freezePreviewButton_Click(object sender, EventArgs e) => TogglePreview(freezeTextBox, freezeTrackBar, freezeStartTextBox, freezePreviewButton, MusicEventType.FreezeTime); + private void startPreviewButton_Click(object sender, EventArgs e) => TogglePreview(startTextBox, startTrackBar, startStartTextBox, startPreviewButton, MusicEventType.RoundStart); + private void bombPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTextBox, bombTrackBar, bombStartTextBox, bombPreviewButton, MusicEventType.BombPlanted); + private void wonPreviewButton_Click(object sender, EventArgs e) => TogglePreview(wonTextBox, wonTrackBar, wonStartTextBox, wonPreviewButton, MusicEventType.RoundWon); + private void lostPreviewButton_Click(object sender, EventArgs e) => TogglePreview(lostTextBox, lostTrackBar, lostStartTextBox, lostPreviewButton, MusicEventType.RoundLost); + private void MVPPreviewButton_Click(object sender, EventArgs e) => TogglePreview(MVPTextBox, MVPTrackBar, MVPStartTextBox, MVPPreviewButton, MusicEventType.Mvp); + private void bombTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(bombTenSecTextBox, bombTenSecTrackBar, bombTenSecStartBox, bombTenSecPreviewButton, MusicEventType.BombTenSeconds); + private void roundTenSecPreviewButton_Click(object sender, EventArgs e) => TogglePreview(roundTenSecTextBox, roundTenSecTrackBar, roundTenSecStartBox, roundTenSecPreviewButton, MusicEventType.RoundTenSeconds); + private void menuPreviewButton_Click(object sender, EventArgs e) => TogglePreview(menuTextBox, menuTrackBar, menuStartTextBox, menuPreviewButton, MusicEventType.MainMenu); private void freezeStartTextbox_KeyPress(object sender, KeyPressEventArgs e) { diff --git a/CS-Jukebox/PreviewCompletedEventArgs.cs b/CS-Jukebox/PreviewCompletedEventArgs.cs new file mode 100644 index 0000000..75c8ce4 --- /dev/null +++ b/CS-Jukebox/PreviewCompletedEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace CS_Jukebox +{ + public enum PreviewCompletionReason + { + MusicEnded, + EventEnded + } + + public sealed class PreviewCompletedEventArgs : EventArgs + { + public PreviewCompletedEventArgs(PreviewCompletionReason reason) + { + Reason = reason; + } + + public PreviewCompletionReason Reason { get; } + } +} diff --git a/CS-Jukebox/Properties.cs b/CS-Jukebox/Properties.cs index 3a18fd5..241da90 100644 --- a/CS-Jukebox/Properties.cs +++ b/CS-Jukebox/Properties.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.IO; +using System.Reflection; using System.Windows.Forms; using Newtonsoft.Json; @@ -17,6 +18,8 @@ static class Properties public static string GameDir = null; public static int MasterVolume = 70; + public static bool LimitPreviewToEventDuration = false; + public static bool DarkTheme = false; public static MusicKit SelectedKit { get { return selectedKit; } @@ -26,7 +29,8 @@ public static MusicKit SelectedKit public static List MusicKits = null; private static string startDir; - private static string appDir; + private static bool dataDirectoryWritable = true; + private const string ConfigResourceName = "CS_Jukebox.gamestate_integration_jukebox.cfg"; public static string KitsDirectory => Path.Combine(startDir ?? GetAppDirectory(), MusicKitsPath); @@ -50,12 +54,16 @@ public static void Save() //Converts settings to json file then saves it public static void SaveProperties() { + if (!dataDirectoryWritable) return; + string dir = Path.Combine(startDir, PropertiesFilePath); PropertiesFile propFile = new PropertiesFile(); propFile.GameDir = GameDir; propFile.SelectedKitName = SelectedKitName; propFile.MasterVolume = MasterVolume; + propFile.LimitPreviewToEventDuration = LimitPreviewToEventDuration; + propFile.DarkTheme = DarkTheme; string jsonFile = JsonConvert.SerializeObject(propFile); try @@ -71,8 +79,7 @@ public static void SaveProperties() //Reads properties file then deserializes it public static void LoadProperties() { - appDir = GetAppDirectory(); - startDir = GetDataDirectory(appDir); + startDir = GetDataDirectory(GetAppDirectory()); Console.WriteLine("App Directory: " + startDir); string dir = Path.Combine(startDir, PropertiesFilePath); @@ -84,10 +91,14 @@ public static void LoadProperties() GameDir = propFile.GameDir; SelectedKitName = propFile.SelectedKitName; MasterVolume = Math.Clamp(propFile.MasterVolume, 0, 100); + LimitPreviewToEventDuration = propFile.LimitPreviewToEventDuration; + DarkTheme = propFile.DarkTheme; } catch (FileNotFoundException) { MasterVolume = 70; + LimitPreviewToEventDuration = false; + DarkTheme = false; } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is JsonException) { @@ -95,6 +106,8 @@ public static void LoadProperties() GameDir = null; SelectedKitName = null; MasterVolume = 70; + LimitPreviewToEventDuration = false; + DarkTheme = false; } } @@ -105,34 +118,29 @@ private static string GetAppDirectory() private static string GetDataDirectory(string applicationDirectory) { - if (CanWriteToDirectory(applicationDirectory)) return applicationDirectory; - - string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - if (string.IsNullOrWhiteSpace(localAppData)) return applicationDirectory; - - string fallbackDirectory = Path.Combine(localAppData, "CS-Jukebox"); - try - { - Directory.CreateDirectory(fallbackDirectory); - } - catch (Exception e) - { - ReportError("Storage directory is unavailable", - "CS Jukebox cannot prepare a writable settings directory.\n\n" + e.Message); - return applicationDirectory; - } - - try + dataDirectoryWritable = CanWriteToDirectory(applicationDirectory); + if (!dataDirectoryWritable) { - MigrateUserData(applicationDirectory, fallbackDirectory); + ReportError( + "Application folder is read-only", + "CS Jukebox stores properties.json and the kits folder beside CS-Jukebox.exe.\n\n" + + "Move CS-Jukebox.exe to a writable folder, such as Documents, then restart the application."); } - catch (Exception e) + else { - ReportError("Existing settings could not be migrated", - "CS Jukebox will use a new writable settings directory.\n\n" + e.Message); + try + { + ImportLegacyUserData(applicationDirectory); + } + catch (Exception e) + { + ReportError( + "Existing settings could not be imported", + "CS Jukebox will continue with portable storage beside the executable.\n\n" + e.Message); + } } - return fallbackDirectory; + return applicationDirectory; } private static bool CanWriteToDirectory(string directory) @@ -152,22 +160,40 @@ private static bool CanWriteToDirectory(string directory) } } - private static void MigrateUserData(string sourceDirectory, string destinationDirectory) + private static void ImportLegacyUserData(string destinationDirectory) { - string sourceProperties = Path.Combine(sourceDirectory, PropertiesFilePath); - string destinationProperties = Path.Combine(destinationDirectory, PropertiesFilePath); - if (File.Exists(sourceProperties) && !File.Exists(destinationProperties)) - File.Copy(sourceProperties, destinationProperties); + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (string.IsNullOrWhiteSpace(localAppData)) return; + + string legacyDirectory = Path.Combine(localAppData, "CS-Jukebox"); + if (!Directory.Exists(legacyDirectory) || + string.Equals( + Path.GetFullPath(legacyDirectory).TrimEnd(Path.DirectorySeparatorChar), + Path.GetFullPath(destinationDirectory).TrimEnd(Path.DirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase)) + { + return; + } - string sourceKits = Path.Combine(sourceDirectory, MusicKitsPath); - string destinationKits = Path.Combine(destinationDirectory, MusicKitsPath); - if (!Directory.Exists(sourceKits)) return; + string legacyProperties = Path.Combine(legacyDirectory, PropertiesFilePath); + string portableProperties = Path.Combine(destinationDirectory, PropertiesFilePath); + string portableKitsDirectory = Path.Combine(destinationDirectory, MusicKitsPath); - foreach (string sourceFile in Directory.GetFiles(sourceKits, "*.json", SearchOption.TopDirectoryOnly)) + // Import only into a completely new portable data directory. This + // prevents deleted kits from reappearing from legacy storage later. + if (File.Exists(portableProperties) || Directory.Exists(portableKitsDirectory)) return; + + if (File.Exists(legacyProperties) && !File.Exists(portableProperties)) + File.Copy(legacyProperties, portableProperties); + + string legacyKitsDirectory = Path.Combine(legacyDirectory, MusicKitsPath); + if (!Directory.Exists(legacyKitsDirectory)) return; + + foreach (string legacyKit in Directory.GetFiles(legacyKitsDirectory, "*.json", SearchOption.TopDirectoryOnly)) { - string destinationFile = Path.Combine(destinationKits, Path.GetFileName(sourceFile)); - Directory.CreateDirectory(destinationKits); - if (!File.Exists(destinationFile)) File.Copy(sourceFile, destinationFile); + Directory.CreateDirectory(portableKitsDirectory); + string portableKit = Path.Combine(portableKitsDirectory, Path.GetFileName(legacyKit)); + if (!File.Exists(portableKit)) File.Copy(legacyKit, portableKit); } } @@ -179,7 +205,6 @@ public static void CreateConfig() string configFileName = Properties.ConfigName; string cfgDir = GameInstallLocator.GetConfigDirectory(Properties.GameDir); string configPath = Path.Combine(cfgDir, configFileName); - string configSrc = Path.Combine(appDir ?? GetAppDirectory(), configFileName); try { @@ -189,7 +214,11 @@ public static void CreateConfig() return; } - File.Copy(configSrc, configPath, true); + using Stream configStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ConfigResourceName); + if (configStream == null) + throw new InvalidOperationException("The embedded GSI configuration resource is missing."); + + WriteStreamAtomically(configPath, configStream); } catch (Exception e) { @@ -199,6 +228,8 @@ public static void CreateConfig() public static void SaveKits() { + if (!dataDirectoryWritable) return; + string dir = KitsDirectory; var saveErrors = new List(); try @@ -233,6 +264,8 @@ public static void LoadKits() { string dir = KitsDirectory; MusicKits = new List(); + if (!dataDirectoryWritable) return; + string[] kitFiles; try @@ -289,6 +322,7 @@ public static void LoadKits() //Deletes the json file for a kit but not the kit itself public static void DeleteKitFile(string kitName) { + if (!dataDirectoryWritable) return; if (!TryValidateKitFileName(kitName, out _)) return; string dir = KitsDirectory; string kitDir = Path.Combine(dir, kitName + ".json"); @@ -375,12 +409,33 @@ private static void WriteAllTextAtomically(string path, string contents) } } + private static void WriteStreamAtomically(string path, Stream source) + { + string directory = Path.GetDirectoryName(Path.GetFullPath(path)); + Directory.CreateDirectory(directory); + string temporaryPath = Path.Combine(directory, + "." + Path.GetFileName(path) + "." + Guid.NewGuid().ToString("N") + ".tmp"); + + try + { + using (var destination = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + source.CopyTo(destination); + File.Move(temporaryPath, path, true); + } + finally + { + try { File.Delete(temporaryPath); } catch { } + } + } + //Inner class for properties parameters private class PropertiesFile { public string GameDir; public string SelectedKitName; public int MasterVolume; + public bool LimitPreviewToEventDuration; + public bool DarkTheme; } } } diff --git a/CS-Jukebox/ThemeManager.cs b/CS-Jukebox/ThemeManager.cs new file mode 100644 index 0000000..3bf9605 --- /dev/null +++ b/CS-Jukebox/ThemeManager.cs @@ -0,0 +1,192 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace CS_Jukebox +{ + internal static class ThemeManager + { + private static readonly Color DarkBackground = Color.FromArgb(31, 35, 42); + private static readonly Color DarkSurface = Color.FromArgb(42, 47, 56); + private static readonly Color DarkControl = Color.FromArgb(52, 58, 69); + private static readonly Color DarkInput = Color.FromArgb(36, 41, 49); + private static readonly Color DarkText = Color.FromArgb(232, 235, 241); + private static readonly Color DarkMutedText = Color.FromArgb(184, 192, 204); + private static readonly Color DarkBorder = Color.FromArgb(82, 91, 106); + private static readonly Color DarkHover = Color.FromArgb(63, 71, 84); + private static readonly Color DarkPressed = Color.FromArgb(72, 99, 142); + private static readonly Color DarkAccent = Color.FromArgb(122, 162, 247); + private static readonly Color DarkError = Color.FromArgb(255, 125, 135); + + private const int DwmUseImmersiveDarkMode = 20; + private const int DwmUseImmersiveDarkModeBefore20H1 = 19; + + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute( + IntPtr windowHandle, + int attribute, + ref int attributeValue, + int attributeSize); + + public static void Apply(Form form) + { + if (form == null || form.IsDisposed) return; + + ApplyControl(form, Properties.DarkTheme); + form.HandleCreated -= Form_HandleCreated; + form.HandleCreated += Form_HandleCreated; + if (form.IsHandleCreated) ApplyTitleBar(form); + form.Invalidate(true); + } + + public static void ApplyToOpenForms() + { + var forms = new Form[Application.OpenForms.Count]; + for (int index = 0; index < forms.Length; index++) + forms[index] = Application.OpenForms[index]; + foreach (Form form in forms) Apply(form); + } + + private static void Form_HandleCreated(object sender, EventArgs e) + { + if (sender is Form form) ApplyTitleBar(form); + } + + private static void ApplyControl(Control control, bool dark) + { + switch (control) + { + case Form form: + form.BackColor = dark ? DarkBackground : SystemColors.Control; + form.ForeColor = dark ? DarkText : SystemColors.ControlText; + break; + + case GroupBox groupBox: + groupBox.BackColor = dark ? DarkSurface : SystemColors.Control; + groupBox.ForeColor = dark ? DarkText : SystemColors.ControlText; + break; + + case Button button: + ApplyButton(button, dark); + break; + + case TextBoxBase textBox: + textBox.BackColor = dark ? DarkInput : SystemColors.Window; + textBox.ForeColor = dark ? DarkText : SystemColors.WindowText; + textBox.BorderStyle = dark ? BorderStyle.FixedSingle : BorderStyle.Fixed3D; + break; + + case ComboBox comboBox: + comboBox.BackColor = dark ? DarkInput : SystemColors.Window; + comboBox.ForeColor = dark ? DarkText : SystemColors.WindowText; + comboBox.FlatStyle = dark ? FlatStyle.Flat : FlatStyle.Standard; + break; + + case ListBox listBox: + listBox.BackColor = dark ? DarkInput : SystemColors.Window; + listBox.ForeColor = dark ? DarkText : SystemColors.WindowText; + listBox.BorderStyle = dark ? BorderStyle.FixedSingle : BorderStyle.Fixed3D; + break; + + case TrackBar trackBar: + trackBar.BackColor = dark + ? trackBar.Parent?.BackColor ?? DarkBackground + : SystemColors.Control; + trackBar.ForeColor = dark ? DarkAccent : SystemColors.ControlText; + break; + + case CheckBox checkBox: + checkBox.BackColor = dark + ? checkBox.Parent?.BackColor ?? DarkBackground + : SystemColors.Control; + checkBox.ForeColor = dark ? DarkText : SystemColors.ControlText; + checkBox.UseVisualStyleBackColor = !dark; + break; + + case LinkLabel linkLabel: + linkLabel.BackColor = dark + ? linkLabel.Parent?.BackColor ?? DarkBackground + : SystemColors.Control; + linkLabel.ForeColor = dark ? DarkAccent : SystemColors.ControlText; + linkLabel.LinkColor = dark ? DarkAccent : SystemColors.HotTrack; + linkLabel.ActiveLinkColor = dark ? DarkText : Color.Red; + break; + + case Label label: + label.BackColor = dark + ? label.Parent?.BackColor ?? DarkBackground + : SystemColors.Control; + label.ForeColor = string.Equals(label.Name, "errorLabel", StringComparison.OrdinalIgnoreCase) + ? dark ? DarkError : Color.Red + : dark ? DarkMutedText : SystemColors.ControlText; + break; + + default: + if (control is Panel or UserControl) + { + control.BackColor = dark ? DarkSurface : SystemColors.Control; + control.ForeColor = dark ? DarkText : SystemColors.ControlText; + } + break; + } + + foreach (Control child in control.Controls) + ApplyControl(child, dark); + } + + private static void ApplyButton(Button button, bool dark) + { + button.UseVisualStyleBackColor = !dark; + button.FlatStyle = dark ? FlatStyle.Flat : FlatStyle.Standard; + button.BackColor = dark ? DarkControl : SystemColors.Control; + button.ForeColor = dark ? DarkText : SystemColors.ControlText; + + if (dark) + { + button.FlatAppearance.BorderColor = DarkBorder; + button.FlatAppearance.BorderSize = 1; + button.FlatAppearance.MouseOverBackColor = DarkHover; + button.FlatAppearance.MouseDownBackColor = DarkPressed; + } + else + { + button.FlatAppearance.BorderColor = Color.Empty; + button.FlatAppearance.MouseOverBackColor = Color.Empty; + button.FlatAppearance.MouseDownBackColor = Color.Empty; + } + } + + private static void ApplyTitleBar(Form form) + { + if (!OperatingSystem.IsWindows() || !form.IsHandleCreated) return; + + int enabled = Properties.DarkTheme ? 1 : 0; + try + { + int result = DwmSetWindowAttribute( + form.Handle, + DwmUseImmersiveDarkMode, + ref enabled, + sizeof(int)); + + if (result != 0) + { + DwmSetWindowAttribute( + form.Handle, + DwmUseImmersiveDarkModeBefore20H1, + ref enabled, + sizeof(int)); + } + } + catch (DllNotFoundException) + { + // Older Windows versions do not expose DWM dark title bars. + } + catch (EntryPointNotFoundException) + { + // Keep the themed client area even when the title-bar API is unavailable. + } + } + } +} diff --git a/CSGSI/CSGSI.csproj b/CSGSI/CSGSI.csproj index be1fd28..01384f0 100644 --- a/CSGSI/CSGSI.csproj +++ b/CSGSI/CSGSI.csproj @@ -20,7 +20,8 @@ TRACE - pdbonly + none + false true @@ -68,4 +69,4 @@ --> - \ No newline at end of file + diff --git a/README.md b/README.md index 9e907fd..c5bb5ae 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Download community-created kits or share your own on our official subreddit: * **Independent Audio Engine:** Powered by [NAudio](https://github.com/naudio/NAudio) for low-latency playback without relying on legacy Windows Media Player components. * **Smart Steam Discovery:** Automatically scans all connected Steam libraries (`libraryfolders.vdf`) across your drives to locate CS2 and configure the integration file. * **Intuitive Kit Dashboard:** Easily create, manage, switch, and test custom audio kits using `.mp3` and `.wav` formats. +* **Comfortable Dark Theme:** Switch the complete interface to a soft blue-graphite palette from the main window; the preference is remembered automatically. --- ## 🚀 Getting Started @@ -61,13 +62,14 @@ Download community-created kits or share your own on our official subreddit: 1. Download the latest compiled release archive: 👉 **[Download CS-Jukebox V2 (Latest Release)](https://github.com/TYFALY/CS-Jukebox-V2/releases/latest)** *(Windows only)* -2. Extract the downloaded archive to any folder on your PC. +2. Place `CS-Jukebox.exe` in any writable folder on your PC. --- ## 📦 Installation & Setup -1. Launch `CS-Jukebox.exe`. +1. Launch `CS-Jukebox.exe`. (To avoid problems, place the application in a separate, empty folder.) + The application creates `properties.json`, `debug_log.txt`, and the `kits` folder beside the executable. Keep these items together when moving an existing portable installation. 2. The application will automatically scan your Steam libraries and detect your CS2 installation directory. > [!TIP] > If automatic detection fails, click **Browse** and manually select your CS2 `game` folder: @@ -86,17 +88,18 @@ Download community-created kits or share your own on our official subreddit: > [!NOTE] > Tracks exceeding the ideal duration will automatically cut off or fade smoothly when the next gameplay event triggers. - -| Event / Track | Minimum Length | Ideal Duration | Behavior & Engine Context | -| :--- | :---: | :---: | :--- | -| **Main Menu** | Loopable | Ambient / Seamless | Loops continuously while in the main menu or lobby. | -| **Freeze Time** | 10 sec | 10–15 sec | Plays during the pre-round buy phase (15s timer). | -| **Round Start** | 5 sec | 5–10 sec | Plays at round start; automatically fades out after ~5s. | -| **Bomb Planted** | 30 sec | 30 sec | Plays on bomb plant (Premier bomb timer is 40s total). | -| **10-Sec Warning** | 10 sec | **Exactly 10 sec** | Plays when exactly 10s remain on round or bomb timer. | -| **MVP Anthem** | **7 sec** | **7 sec** | Plays when you earn round MVP; fades into the next freeze time. | -| **Round Won / Lost** | **7 sec** | **7 sec** | Fallback round-end track played when you are not the MVP. | -| **Player Death** | 3 sec | 3–5 sec | Secondary audio cue triggered immediately when you die. | +> In the Music Selector, enable **Limit preview to event duration** to simulate the fixed preview window below. **Start At** changes the source offset, not the length of that window. The option also applies to previews in **Extra tracks**. + +| Event / Track | Minimum Length | Ideal Duration | Limited Preview | Behavior & Engine Context | +| :--- | :---: | :---: | :---: | :--- | +| **Main Menu** | Loopable | Ambient / Seamless | Until file ends | Loops continuously while in the main menu or lobby. | +| **Freeze Time** | 10 sec | 10–15 sec | **15 sec** | Plays during the pre-round buy phase (15s timer). | +| **Round Start** | 5 sec | 5–10 sec | **5 sec** | Plays at round start; automatically fades out after ~5s. | +| **Bomb Planted** | 30 sec | 30 sec | **30 sec** | Plays on bomb plant (Premier bomb timer is 40s total). | +| **10-Sec Warning** | 10 sec | **Exactly 10 sec** | **10 sec** | Plays when exactly 10s remain on round or bomb timer. | +| **MVP Anthem** | **7 sec** | **7 sec** | **7 sec** | Plays when you earn round MVP; fades into the next freeze time. | +| **Round Won / Lost** | **7 sec** | **7 sec** | **7 sec** | Fallback round-end track played when you are not the MVP. | +| **Player Death** | 3 sec | 3–5 sec | **5 sec** | Secondary audio cue triggered immediately when you die. | ### How to Create & Assign Kits @@ -138,18 +141,20 @@ Make sure you have the following installed: git clone https://github.com/TYFALY/CS-Jukebox-V2.git cd CS-Jukebox-V2 -# 2. Build the project in Release configuration -dotnet build -c Release +# 2. Publish the compressed, self-contained single-file release +dotnet publish ./CS-Jukebox/CS-Jukebox.csproj -c Release ``` ### Output Location -After a successful build, the executable will be available at: +After a successful publish, the only distributed file will be available at: ```text -bin/Release/net8.0-windows/CS-Jukebox.exe +CS-Jukebox/bin/Release/net8.0-windows/win-x64/publish/CS-Jukebox.exe ``` +The GSI configuration is embedded in the executable and installed into the CS2 configuration directory automatically. Runtime settings and music kits remain portable beside `CS-Jukebox.exe`. + --- ## ❓ Troubleshooting @@ -168,7 +173,7 @@ CS-Jukebox scans your Steam directories via `libraryfolders.vdf`. If automatic d 1. Ensure `CS-Jukebox.exe` is running while playing. 2. Verify that the GSI config file was created at: - `...\Counter-Strike Global Offensive\game\csgo\cfg\gamestate_integration_csjukebox.cfg` + `...\Counter-Strike Global Offensive\game\csgo\cfg\gamestate_integration_jukebox.cfg` 3. Restart Counter-Strike 2 so the game engine loads the integration file. 4. Verify your audio tracks are valid, uncorrupted `.mp3` or `.wav` files.